nansen-cli 1.9.3 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -17,159 +17,12 @@ const { version: VERSION } = require('../package.json');
17
17
 
18
18
  // ============= Schema Definition =============
19
19
 
20
- export const SCHEMA = {
21
- version: VERSION,
22
- commands: {
23
- 'research': {
24
- description: 'Research and analytics commands',
25
- subcommands: {
26
- 'smart-money': {
27
- description: 'Smart Money analytics - track sophisticated market participants',
28
- subcommands: {
29
- 'netflow': {
30
- description: 'Net capital flows (inflows vs outflows)',
31
- options: {
32
- chain: { type: 'string', default: 'solana', description: 'Blockchain to query' },
33
- chains: { type: 'array', description: 'Multiple chains as JSON array' },
34
- limit: { type: 'number', description: 'Number of results' },
35
- labels: { type: 'string|array', description: 'Smart Money label filter' },
36
- sort: { type: 'string', description: 'Sort field:direction (e.g., value_usd:desc)' },
37
- filters: { type: 'object', description: 'Additional filters as JSON' }
38
- },
39
- returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'inflow_usd', 'outflow_usd', 'net_flow_usd']
40
- },
41
- 'dex-trades': {
42
- description: 'Real-time DEX trading activity',
43
- options: { chain: { type: 'string', default: 'solana' }, chains: { type: 'array' }, limit: { type: 'number' }, labels: { type: 'string|array' }, sort: { type: 'string' }, filters: { type: 'object' } },
44
- returns: ['chain', 'block_timestamp', 'transaction_hash', 'trader_address', 'trader_address_label', 'token_bought_address', 'token_sold_address', 'token_bought_amount', 'token_sold_amount', 'token_bought_symbol', 'token_sold_symbol', 'trade_value_usd']
45
- },
46
- 'perp-trades': {
47
- description: 'Perpetual trading on Hyperliquid',
48
- options: { limit: { type: 'number' }, sort: { type: 'string' }, filters: { type: 'object' } },
49
- returns: ['trader_address', 'trader_address_label', 'token_symbol', 'side', 'action', 'token_amount', 'price_usd', 'value_usd', 'type', 'block_timestamp', 'transaction_hash']
50
- },
51
- 'holdings': {
52
- description: 'Aggregated token balances',
53
- options: { chain: { type: 'string', default: 'solana' }, chains: { type: 'array' }, limit: { type: 'number' }, labels: { type: 'string|array' } },
54
- returns: ['chain', 'token_address', 'token_symbol', 'token_sectors', 'value_usd', 'balance_24h_percent_change', 'holders_count', 'share_of_holdings_percent', 'token_age_days', 'market_cap_usd']
55
- },
56
- 'dcas': {
57
- description: 'DCA strategies on Jupiter',
58
- options: { limit: { type: 'number' }, filters: { type: 'object' } },
59
- returns: ['dca_created_at', 'dca_updated_at', 'trader_address', 'trader_address_label', 'dca_vault_address', 'input_token_address', 'output_token_address', 'deposit_token_amount', 'token_spent_amount', 'output_token_redeemed_amount', 'dca_status', 'input_token_symbol', 'output_token_symbol', 'deposit_value_usd']
60
- },
61
- 'historical-holdings': {
62
- description: 'Historical holdings over time',
63
- options: { chain: { type: 'string', default: 'solana' }, chains: { type: 'array' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
64
- returns: ['date', 'token_address', 'token_symbol', 'balance', 'balance_usd']
65
- }
66
- }
67
- },
68
- 'profiler': {
69
- description: 'Wallet profiling - detailed information about any blockchain address',
70
- subcommands: {
71
- 'balance': { description: 'Current token holdings', options: { address: { type: 'string', required: true, description: 'Wallet address to query' }, chain: { type: 'string', default: 'ethereum' }, entity: { type: 'string', description: 'Entity name instead of address' } }, returns: ['chain', 'address', 'token_address', 'token_symbol', 'token_name', 'token_amount', 'price_usd', 'value_usd'] },
72
- 'labels': { description: 'Behavioral and entity labels', options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' } }, returns: ['label', 'label_type', 'label_subtype'] },
73
- 'transactions': { description: 'Transaction history', options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, date: { type: 'string', required: true, description: 'Date or date range' }, limit: { type: 'number' }, days: { type: 'number', default: 30 } }, returns: ['chain', 'method', 'tokens_sent', 'tokens_received', 'volume_usd', 'block_timestamp', 'transaction_hash'] },
74
- 'pnl': { description: 'PnL and trade performance', options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, date: { type: 'string', description: 'Date or date range' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } }, returns: ['token_address', 'token_symbol', 'realized_pnl_usd', 'unrealized_pnl_usd', 'total_pnl_usd'] },
75
- 'search': { description: 'Search for entities by name', options: { query: { type: 'string', required: true, description: 'Search query' }, limit: { type: 'number' } }, returns: ['entity_name'] },
76
- 'historical-balances': { description: 'Historical balances over time', options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, days: { type: 'number', default: 30 } }, returns: ['date', 'token_address', 'token_symbol', 'balance', 'balance_usd'] },
77
- 'related-wallets': { description: 'Find wallets related to an address', options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, limit: { type: 'number' } }, returns: ['address', 'address_label', 'relation', 'transaction_hash', 'block_timestamp', 'order', 'chain'] },
78
- 'counterparties': { description: 'Top counterparties by volume', options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, days: { type: 'number', default: 30 } }, returns: ['counterparty_address', 'counterparty_address_label', 'interaction_count', 'total_volume_usd', 'volume_in_usd', 'volume_out_usd', 'tokens_info'] },
79
- 'pnl-summary': { description: 'Summarized PnL metrics', options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, days: { type: 'number', default: 30 } }, returns: ['top5_tokens', 'traded_token_count', 'traded_times', 'realized_pnl_usd', 'realized_pnl_percent', 'win_rate'] },
80
- 'perp-positions': { description: 'Current perpetual positions', options: { address: { type: 'string', required: true }, limit: { type: 'number' } }, returns: ['symbol', 'side', 'size', 'entry_price', 'mark_price', 'unrealized_pnl', 'leverage'] },
81
- 'perp-trades': { description: 'Perpetual trading history', options: { address: { type: 'string', required: true }, days: { type: 'number', default: 30 }, limit: { type: 'number' } }, returns: ['symbol', 'side', 'size', 'price', 'value_usd', 'pnl_usd', 'timestamp'] },
82
- 'batch': { description: 'Batch profile multiple addresses', options: { addresses: { type: 'string', description: 'Comma-separated addresses' }, file: { type: 'string', description: 'File with one address per line' }, chain: { type: 'string', default: 'ethereum' }, include: { type: 'string', default: 'labels,balance', description: 'Comma-separated: labels,balance,pnl' }, delay: { type: 'number', default: 1000, description: 'Delay between requests in ms' } }, returns: ['address', 'chain', 'labels', 'balance', 'pnl', 'error'] },
83
- 'trace': { description: 'Multi-hop counterparty trace (BFS)', options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, depth: { type: 'number', default: 2, description: 'Max hops (1-5)' }, width: { type: 'number', default: 10, description: 'Top N counterparties per hop' }, days: { type: 'number', default: 30 }, delay: { type: 'number', default: 1000, description: 'Delay between requests in ms' } }, returns: ['root', 'chain', 'depth', 'nodes', 'edges', 'stats'] },
84
- 'compare': { description: 'Compare two wallets (shared counterparties, tokens)', options: { addresses: { type: 'string', required: true, description: 'Two comma-separated addresses' }, chain: { type: 'string', default: 'ethereum' }, days: { type: 'number', default: 30 } }, returns: ['addresses', 'chain', 'shared_counterparties', 'shared_tokens', 'balances'] }
85
- }
86
- },
87
- 'token': {
88
- description: 'Token God Mode - deep analytics for any token',
89
- subcommands: {
90
- 'indicators': { description: 'Risk and reward indicators for a token (Nansen Score)', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'ethereum' } }, returns: ['token_info[market_cap_usd, market_cap_group, is_stablecoin]', 'risk_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]', 'reward_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]'] },
91
- 'ohlcv': { description: 'OHLCV candle data for a token', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'solana' }, timeframe: { type: 'string', default: '1h', enum: ['1m', '5m', '15m', '30m', '1h', '2h', '4h', '1d', '1w', '1M'], description: 'Candle timeframe (e.g., 1h, 4h, 1d)' }, limit: { type: 'number' } }, returns: ['timestamp', 'open', 'high', 'low', 'close', 'volume'] },
92
- 'info': { description: 'Get detailed information for a specific token', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'solana' }, timeframe: { type: 'string', default: '1d', enum: ['5m', '1h', '6h', '12h', '1d', '7d'] } }, returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd'] },
93
- 'screener': { description: 'Discover and filter tokens', options: { chain: { type: 'string', default: 'solana' }, chains: { type: 'array' }, timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] }, 'smart-money': { type: 'boolean', description: 'Filter for Smart Money only' }, search: { type: 'string', description: 'Filter results by token symbol or name (client-side)' }, limit: { type: 'number' }, sort: { type: 'string' } }, returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'smart_money_holders'] },
94
- 'holders': { description: 'Token holder analysis', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, 'smart-money': { type: 'boolean' }, limit: { type: 'number' } }, returns: ['address', 'address_label', 'token_amount', 'total_outflow', 'total_inflow', 'balance_change_24h', 'balance_change_7d', 'balance_change_30d', 'ownership_percentage', 'value_usd'] },
95
- 'flows': { description: 'Token flow metrics', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, date: { type: 'string', required: true, description: 'Date or date range' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } }, returns: ['date', 'price_usd', 'token_amount', 'value_usd', 'holders_count', 'total_inflows_count', 'total_outflows_count'] },
96
- 'dex-trades': { description: 'DEX trading activity', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, 'smart-money': { type: 'boolean' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } }, returns: ['tx_hash', 'wallet_address', 'side', 'amount', 'price_usd', 'value_usd', 'timestamp'] },
97
- 'pnl': { description: 'PnL leaderboard', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, days: { type: 'number', default: 30 }, limit: { type: 'number' }, sort: { type: 'string' } }, returns: ['wallet_address', 'realized_pnl_usd', 'unrealized_pnl_usd', 'total_pnl_usd', 'labels'] },
98
- 'who-bought-sold': { description: 'Recent buyers and sellers', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, date: { type: 'string', required: true, description: 'Date or date range' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } }, returns: ['address', 'address_label', 'bought_token_volume', 'sold_token_volume', 'token_trade_volume', 'bought_volume_usd', 'sold_volume_usd', 'trade_volume_usd'] },
99
- 'flow-intelligence': { description: 'Detailed flow intelligence by label', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, days: { type: 'number', default: 30 } }, returns: ['public_figure_net_flow_usd', 'public_figure_wallet_count', 'top_pnl_net_flow_usd', 'top_pnl_wallet_count', 'whale_net_flow_usd', 'whale_wallet_count', 'smart_trader_net_flow_usd', 'smart_trader_wallet_count', 'exchange_net_flow_usd', 'exchange_wallet_count', 'fresh_wallets_net_flow_usd', 'fresh_wallets_wallet_count'] },
100
- 'transfers': { description: 'Token transfer history', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, days: { type: 'number', default: 30 }, limit: { type: 'number' }, from: { type: 'string', description: 'Filter by sender address' }, to: { type: 'string', description: 'Filter by recipient address' }, enrich: { type: 'boolean', description: 'Enrich addresses with Nansen labels' } }, returns: ['tx_hash', 'from', 'to', 'amount', 'value_usd', 'timestamp'] },
101
- 'jup-dca': { description: 'Jupiter DCA orders for token', options: { token: { type: 'string', required: true }, limit: { type: 'number' } }, returns: ['wallet_address', 'input_token', 'output_token', 'total_input', 'executed', 'remaining'] },
102
- 'perp-trades': { description: 'Perp trades by token symbol', options: { symbol: { type: 'string', required: true, description: 'Token symbol (e.g., BTC, ETH)' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } }, returns: ['wallet_address', 'side', 'size', 'price', 'value_usd', 'pnl_usd', 'timestamp'] },
103
- 'perp-positions': { description: 'Open perp positions by token symbol', options: { symbol: { type: 'string', required: true }, limit: { type: 'number' } }, returns: ['wallet_address', 'side', 'size', 'entry_price', 'mark_price', 'unrealized_pnl', 'leverage'] },
104
- 'perp-pnl-leaderboard': { description: 'Perp PnL leaderboard by token', options: { symbol: { type: 'string', required: true }, days: { type: 'number', default: 30 }, limit: { type: 'number' } }, returns: ['wallet_address', 'realized_pnl', 'unrealized_pnl', 'total_pnl', 'trade_count'] }
105
- }
106
- },
107
- 'search': {
108
- description: 'Search for tokens and entities across Nansen',
109
- options: {
110
- query: { type: 'string', required: true, description: 'Search query (token name, symbol, address, or entity)' },
111
- type: { type: 'string', default: 'any', enum: ['token', 'entity', 'any'], description: 'Result type filter' },
112
- chain: { type: 'string', description: 'Filter by chain (e.g., ethereum, solana)' },
113
- limit: { type: 'number', default: 25, description: 'Max results (1-50)' }
114
- },
115
- returns: ['tokens[name, symbol, chain, address, price, volume_24h, market_cap, rank]', 'entities[name, tags, rank]', 'total_results']
116
- },
117
- 'perp': {
118
- description: 'Perpetual futures analytics',
119
- subcommands: {
120
- 'screener': { description: 'Screen perpetual futures contracts', options: { days: { type: 'number', default: 30 }, limit: { type: 'number' }, sort: { type: 'string' }, filters: { type: 'object' } }, returns: ['token_symbol', 'volume_usd', 'open_interest', 'funding_rate', 'price_change_24h'] },
121
- 'leaderboard': { description: 'Perpetual futures PnL leaderboard', options: { days: { type: 'number', default: 30 }, limit: { type: 'number' }, sort: { type: 'string' }, filters: { type: 'object' } }, returns: ['address', 'address_label', 'realized_pnl', 'unrealized_pnl', 'total_pnl', 'trade_count', 'win_rate'] }
122
- }
123
- },
124
- 'portfolio': {
125
- description: 'Portfolio analytics',
126
- subcommands: {
127
- 'defi': { description: 'DeFi holdings across protocols', options: { wallet: { type: 'string', required: true, description: 'Wallet address' } }, returns: ['protocol', 'chain', 'position_type', 'token_symbol', 'balance', 'balance_usd'] }
128
- }
129
- },
130
- 'points': {
131
- description: 'Nansen Points analytics',
132
- subcommands: {
133
- 'leaderboard': { description: 'Points leaderboard', options: { tier: { type: 'string', description: 'Filter by tier' }, limit: { type: 'number' } }, returns: ['rank', 'address', 'address_label', 'points', 'tier'] }
134
- }
135
- }
136
- }
137
- },
138
- 'trade': {
139
- description: 'DEX trading commands',
140
- subcommands: {
141
- 'quote': {
142
- description: 'Get a DEX swap quote (chain, tokens, amount)',
143
- options: {
144
- chain: { type: 'string', default: 'ethereum', description: 'Blockchain' },
145
- from: { type: 'string', required: true, description: 'Token to sell (address or symbol)' },
146
- to: { type: 'string', required: true, description: 'Token to buy (address or symbol)' },
147
- amount: { type: 'string', required: true, description: 'Amount to swap' },
148
- wallet: { type: 'string', description: 'Wallet name, or "walletconnect"/"wc" for WalletConnect (EVM only)' }
149
- }
150
- },
151
- 'execute': {
152
- description: 'Sign and broadcast a quoted trade',
153
- options: {
154
- chain: { type: 'string', default: 'ethereum', description: 'Blockchain' },
155
- wallet: { type: 'string', description: 'Wallet name, or "walletconnect"/"wc" for WalletConnect (EVM only)' }
156
- }
157
- }
158
- }
159
- }
160
- },
161
- globalOptions: {
162
- pretty: { type: 'boolean', description: 'Format JSON output for readability' },
163
- table: { type: 'boolean', description: 'Format output as human-readable table' },
164
- fields: { type: 'string', description: 'Comma-separated list of fields to include in output' },
165
- 'no-retry': { type: 'boolean', description: 'Disable automatic retry on rate limits/errors' },
166
- retries: { type: 'number', default: 3, description: 'Max retry attempts' },
167
- format: { type: 'string', enum: ['json', 'csv'], description: 'Output format (default: json)' },
168
- 'x402-payment-signature': { type: 'string', description: 'Pre-signed x402 payment signature header' }
169
- },
170
- chains: ['ethereum', 'solana', 'base', 'bnb', 'arbitrum', 'polygon', 'optimism', 'avalanche', 'linea', 'scroll', 'mantle', 'ronin', 'sei', 'plasma', 'sonic', 'monad', 'hyperevm', 'iotaevm'],
171
- smartMoneyLabels: ['Fund', 'Smart Trader', '30D Smart Trader', '90D Smart Trader', '180D Smart Trader', 'Smart HL Perps Trader']
172
- };
20
+ const schemaDefinition = require('./schema.json');
21
+
22
+ // SCHEMA is the static definition with version injected at runtime.
23
+ // The schema.json file is the source of truth for command metadata (returns, options, etc.)
24
+ // and should be updated whenever the API changes — do not edit returns arrays here.
25
+ export const SCHEMA = { version: VERSION, ...schemaDefinition };
173
26
 
174
27
  // ============= Field Filtering =============
175
28
 
@@ -220,7 +73,52 @@ export function parseFields(fieldsOption) {
220
73
  return fieldsOption.split(',').map(f => f.trim()).filter(f => f.length > 0);
221
74
  }
222
75
 
223
- // Parse command line arguments
76
+ /**
77
+ * Produce a compact schema listing commands with params* notation.
78
+ * Use `nansen schema --full` for the verbose version.
79
+ */
80
+ export function compactSchema(schema) {
81
+ function compactOptions(opts) {
82
+ if (!opts) return '';
83
+ return Object.entries(opts)
84
+ .map(([name, o]) => `${name}${o.required ? '*' : ''}`)
85
+ .join(', ');
86
+ }
87
+
88
+ function compactCmd(prefix, cmd) {
89
+ const entries = [];
90
+ if (cmd.subcommands) {
91
+ for (const [name, sub] of Object.entries(cmd.subcommands)) {
92
+ const path = prefix ? `${prefix} ${name}` : name;
93
+ if (sub.subcommands) {
94
+ entries.push(...compactCmd(path, sub));
95
+ } else {
96
+ const params = compactOptions(sub.options);
97
+ entries.push({ command: path, description: sub.description, params, returns: sub.returns });
98
+ }
99
+ }
100
+ } else {
101
+ const params = compactOptions(cmd.options);
102
+ entries.push({ command: prefix, description: cmd.description, params, returns: cmd.returns });
103
+ }
104
+ return entries;
105
+ }
106
+
107
+ const commands = [];
108
+ for (const [name, cmd] of Object.entries(schema.commands)) {
109
+ commands.push(...compactCmd(name, cmd));
110
+ }
111
+
112
+ return {
113
+ version: schema.version,
114
+ params_legend: '* = required',
115
+ commands,
116
+ globalOptions: Object.keys(schema.globalOptions).join(', '),
117
+ chains: schema.chains,
118
+ smartMoneyLabels: schema.smartMoneyLabels
119
+ };
120
+ }
121
+
224
122
  /**
225
123
  * Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
226
124
  */
@@ -244,7 +142,7 @@ export function parseArgs(args) {
244
142
  const key = arg.slice(2);
245
143
  const next = args[i + 1];
246
144
 
247
- if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich') {
145
+ if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full') {
248
146
  result.flags[key] = true;
249
147
  } else if (next && !next.startsWith('-')) {
250
148
  // Try to parse as JSON first (for objects/arrays/booleans),
@@ -635,7 +533,7 @@ export async function traceCounterparties(api, params = {}) {
635
533
  queue.push({ addr: cpAddr, hop: hop + 1 });
636
534
  }
637
535
  }
638
- } catch (err) {
536
+ } catch {
639
537
  // Skip addresses that fail (404, etc) but continue the traversal
640
538
  }
641
539
 
@@ -706,91 +604,41 @@ export async function compareWallets(api, params = {}) {
706
604
  };
707
605
  }
708
606
 
709
- // ASCII Art Banner
710
- export const BANNER = `
711
- ███╗ ██╗ █████╗ ███╗ ██╗███████╗███████╗███╗ ██╗
712
- ████╗ ██║██╔══██╗████╗ ██║██╔════╝██╔════╝████╗ ██║
713
- ██╔██╗ ██║███████║██╔██╗ ██║███████╗█████╗ ██╔██╗ ██║
714
- ██║╚██╗██║██╔══██║██║╚██╗██║╚════██║██╔══╝ ██║╚██╗██║
715
- ██║ ╚████║██║ ██║██║ ╚████║███████║███████╗██║ ╚████║
716
- ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═══╝
717
- Surface The Signal
718
- `;
607
+ export const BANNER = '';
719
608
 
720
- // Help text
721
- export const HELP = `
722
- Nansen CLI - Command-line interface for Nansen API
723
- Designed for AI agents with structured JSON output.
609
+ export const HELP = `Nansen CLI v${VERSION} — structured JSON output for AI agents.
724
610
 
725
- USAGE:
726
- nansen <command> [subcommand] [options]
611
+ USAGE: nansen <command> [subcommand] [options]
727
612
 
728
613
  COMMANDS:
729
- research Research & analytics (smart-money, profiler, token, search, perp, portfolio, points)
730
- trade DEX trading (quote, execute)
731
- wallet Local wallet management (create, list, show, export, default, delete)
732
- login/logout API key management
733
- schema Output JSON schema for all commands (for agent introspection)
734
- cache Cache management (clear)
735
- changelog Show what's new (use --since <version> to filter)
736
- help Show this help message
737
-
738
- GLOBAL OPTIONS:
739
- --pretty Format JSON output for readability
740
- --table Format output as human-readable table
741
- --fields Comma-separated list of fields to include (e.g., --fields address,value_usd)
742
- --chain Blockchain to query (ethereum, solana, base, etc.)
743
- --chains Multiple chains as JSON array
744
- --limit Number of results (shorthand for pagination)
745
- --filters JSON object with filters
746
- --sort Sort by field (e.g., --sort value_usd:desc)
747
- --order-by JSON array with sort order (advanced)
748
- --days Date range in days (default: 30 for most endpoints)
749
- --symbol Token symbol (for perp endpoints)
750
- --no-retry Disable automatic retry on rate limits/errors
751
- --retries <n> Max retry attempts (default: 3)
752
- --x402-payment-signature <sig> Pre-signed x402 payment signature header
753
- --cache Enable response caching (default: off)
754
- --no-cache Disable cache for this request
755
- --cache-ttl <s> Cache TTL in seconds (default: 300)
756
- --stream Output as JSON lines (NDJSON) for incremental processing
757
- --format csv Output as CSV with header row
614
+ research smart-money, profiler, token, search, perp, portfolio, points
615
+ trade quote, execute
616
+ wallet create, list, show, export, default, delete
617
+ login Save API key (--api-key <key> or interactive)
618
+ logout Remove saved API key
619
+ schema JSON schema for all commands (use "nansen schema <cmd>" for one)
620
+ cache clear
621
+ changelog --since <version> to filter
622
+
623
+ OPTIONS: --chain --limit --sort field:dir --fields a,b --days N --filters '{}'
624
+ FORMAT: --pretty --table --format csv --stream (NDJSON)
625
+ RETRY: --no-retry --retries N --cache --cache-ttl N
758
626
 
759
627
  EXAMPLES:
760
- # Get Smart Money netflow on Solana
761
628
  nansen research smart-money netflow --chain solana
762
-
763
- # Get top tokens by Smart Money activity
764
- nansen research token screener --chain solana --timeframe 24h --pretty
765
-
766
- # Get wallet balance
767
- nansen research profiler balance --address 0x123... --chain ethereum
768
-
769
- # Search for tokens/entities
770
- nansen research search "Vitalik"
771
-
772
- # Get a DEX swap quote
629
+ nansen research token screener --chain solana --timeframe 24h
630
+ nansen research profiler balance --address 0x... --chain ethereum
773
631
  nansen trade quote --chain ethereum --from ETH --to USDC --amount 1
774
632
 
775
- SMART MONEY LABELS:
776
- Fund, Smart Trader, 30D Smart Trader, 90D Smart Trader,
777
- 180D Smart Trader, Smart HL Perps Trader
778
-
779
- SUPPORTED CHAINS:
780
- ethereum, solana, base, bnb, arbitrum, polygon, optimism,
781
- avalanche, linea, scroll, mantle, ronin, sei,
782
- plasma, sonic, monad, hyperevm, iotaevm
633
+ Chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
634
+ Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
783
635
 
784
- For more info: https://docs.nansen.ai
636
+ Docs: https://docs.nansen.ai
637
+ Skills: npx skills add nansen-ai/nansen-cli (agent-optimised docs per command group)
785
638
  `;
786
639
 
787
640
  // Helper to prompt for input (exported for mocking)
788
641
  export async function prompt(question, hidden = false) {
789
- const rl = readline.createInterface({
790
- input: process.stdin,
791
- output: process.stdout
792
- });
793
-
794
642
  return new Promise((resolve) => {
795
643
  if (hidden && process.stdout.isTTY) {
796
644
  process.stdout.write(question);
@@ -805,7 +653,6 @@ export async function prompt(question, hidden = false) {
805
653
  process.stdin.pause();
806
654
  process.stdin.removeListener('data', onData);
807
655
  process.stdout.write('\n');
808
- rl.close();
809
656
  resolve(input);
810
657
  } else if (char === '\u0003') {
811
658
  // Ctrl+C
@@ -824,6 +671,10 @@ export async function prompt(question, hidden = false) {
824
671
 
825
672
  process.stdin.on('data', onData);
826
673
  } else {
674
+ const rl = readline.createInterface({
675
+ input: process.stdin,
676
+ output: process.stdout
677
+ });
827
678
  rl.question(question, (answer) => {
828
679
  rl.close();
829
680
  resolve(answer);
@@ -836,11 +687,11 @@ export async function prompt(question, hidden = false) {
836
687
  export function buildCommands(deps = {}) {
837
688
  // Allow dependency injection for testing
838
689
  const {
839
- api = null,
690
+ api: _api = null,
840
691
  promptFn = prompt,
841
692
  log = console.log,
842
- errorOutput = console.error,
843
- NansenAPIClass = NansenAPI,
693
+ errorOutput: _errorOutput = console.error,
694
+ NansenAPIClass: _NansenAPIClass = NansenAPI,
844
695
  saveConfigFn = saveConfig,
845
696
  deleteConfigFn = deleteConfig,
846
697
  getConfigFileFn = getConfigFile,
@@ -850,6 +701,18 @@ export function buildCommands(deps = {}) {
850
701
 
851
702
  const cmds = {
852
703
  'login': async (args, apiInstance, flags, options) => {
704
+ if (flags.help || flags.h) {
705
+ log('nansen login - Save your Nansen API key\n');
706
+ log('USAGE:');
707
+ log(' nansen login (interactive)');
708
+ log(' nansen login --api-key <key> (non-interactive)\n');
709
+ log('OPTIONS:');
710
+ log(' --api-key <key> Your Nansen API key');
711
+ log(' --help Show this help\n');
712
+ log('Get your API key at: https://app.nansen.ai/api');
713
+ return;
714
+ }
715
+
853
716
  // Support non-interactive: nansen login --api-key <key>
854
717
  let apiKey = options['api-key'] || options.apiKey;
855
718
 
@@ -885,10 +748,10 @@ export function buildCommands(deps = {}) {
885
748
 
886
749
  log(`✓ Saved to ${getConfigFileFn()}\n`);
887
750
  log('You can now use the Nansen CLI. Try:');
888
- log(' nansen token screener --chain solana --pretty');
751
+ log(' nansen research token screener --chain solana --pretty');
889
752
  },
890
753
 
891
- 'logout': async (args, apiInstance, flags, options) => {
754
+ 'logout': async (_args, _apiInstance, _flags, _options) => {
892
755
  const deleted = deleteConfigFn();
893
756
  if (deleted) {
894
757
  log(`✓ Removed ${getConfigFileFn()}`);
@@ -897,11 +760,11 @@ export function buildCommands(deps = {}) {
897
760
  }
898
761
  },
899
762
 
900
- 'help': async (args, apiInstance, flags, options) => {
763
+ 'help': async (_args, _apiInstance, _flags, _options) => {
901
764
  log(HELP);
902
765
  },
903
766
 
904
- 'changelog': async (args, apiInstance, flags, options) => {
767
+ 'changelog': async (_args, _apiInstance, _flags, _options) => {
905
768
  const changelogPath = new URL('../CHANGELOG.md', import.meta.url).pathname;
906
769
  let content;
907
770
  try {
@@ -910,7 +773,7 @@ export function buildCommands(deps = {}) {
910
773
  log('CHANGELOG.md not found. Visit https://github.com/nansen-ai/nansen-cli/blob/main/CHANGELOG.md');
911
774
  return;
912
775
  }
913
- const since = options.since;
776
+ const since = _options.since;
914
777
  if (since) {
915
778
  // Show only entries from the given version onwards
916
779
  const lines = content.split('\n');
@@ -936,8 +799,7 @@ export function buildCommands(deps = {}) {
936
799
  }
937
800
  },
938
801
 
939
- 'schema': async (args, apiInstance, flags, options) => {
940
- // Return schema for agent introspection
802
+ 'schema': async (args, _apiInstance, flags, _options) => {
941
803
  const subcommand = args[0];
942
804
  const schemaEntry = subcommand && (SCHEMA.commands[subcommand] || SCHEMA.commands.research.subcommands[subcommand]);
943
805
 
@@ -951,11 +813,14 @@ export function buildCommands(deps = {}) {
951
813
  };
952
814
  }
953
815
 
954
- // Return full schema
955
- return SCHEMA;
816
+ if (flags.full) {
817
+ return SCHEMA;
818
+ }
819
+
820
+ return compactSchema(SCHEMA);
956
821
  },
957
822
 
958
- 'cache': async (args, apiInstance, flags, options) => {
823
+ 'cache': async (args, _apiInstance, _flags, _options) => {
959
824
  const subcommand = args[0] || 'help';
960
825
 
961
826
  const handlers = {
@@ -1004,7 +869,7 @@ export function buildCommands(deps = {}) {
1004
869
  const handlers = {
1005
870
  'netflow': () => apiInstance.smartMoneyNetflow({ chains, filters, orderBy, pagination }),
1006
871
  'dex-trades': () => apiInstance.smartMoneyDexTrades({ chains, filters, orderBy, pagination }),
1007
- 'perp-trades': () => apiInstance.smartMoneyPerpTrades({ filters, orderBy, pagination }),
872
+ 'perp-trades': () => apiInstance.smartMoneyPerpTrades({ filters, orderBy, pagination, onlyNewPositions: options['only-new-positions'] ?? flags['only-new-positions'] }),
1008
873
  'holdings': () => apiInstance.smartMoneyHoldings({ chains, filters, orderBy, pagination }),
1009
874
  'dcas': () => apiInstance.smartMoneyDcas({ filters, orderBy, pagination }),
1010
875
  'historical-holdings': () => apiInstance.smartMoneyHistoricalHoldings({ chains, filters, orderBy, pagination, days }),
@@ -1041,7 +906,7 @@ export function buildCommands(deps = {}) {
1041
906
  }
1042
907
  const filters = options.filters || {};
1043
908
  const orderBy = parseSort(options.sort, options['order-by']);
1044
- const pagination = options.limit ? { page: 1, recordsPerPage: options.limit } : undefined;
909
+ const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
1045
910
  const days = options.days ? parseInt(options.days) : 30;
1046
911
 
1047
912
  const handlers = {
@@ -1139,7 +1004,7 @@ export function buildCommands(deps = {}) {
1139
1004
 
1140
1005
  const handlers = {
1141
1006
  'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
1142
- 'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d', pagination }),
1007
+ 'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d' }),
1143
1008
  'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
1144
1009
  'screener': async () => {
1145
1010
  const search = options.search;
@@ -1299,7 +1164,7 @@ export function buildCommands(deps = {}) {
1299
1164
  }
1300
1165
  const category = RESEARCH_CATEGORY_ALIASES[rawCategory] || rawCategory;
1301
1166
  if (!RESEARCH_CATEGORIES.has(category)) {
1302
- return { error: `Unknown research category: ${rawCategory}`, available: [...RESEARCH_CATEGORIES] };
1167
+ throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES].join(', ')}`, ErrorCode.UNKNOWN);
1303
1168
  }
1304
1169
  return cmds[category](args.slice(1), apiInstance, flags, options);
1305
1170
  };
@@ -1330,15 +1195,12 @@ WALLET:
1330
1195
  Defaults to the default local wallet if omitted.
1331
1196
 
1332
1197
  SYMBOLS:
1333
- Common tokens resolve automatically: SOL, ETH, BNB, USDC, USDT, WETH, WBNB
1198
+ Common tokens resolve automatically: SOL, ETH, USDC, USDT, WETH
1334
1199
  Raw addresses are also accepted.`);
1335
1200
  return;
1336
1201
  }
1337
1202
  if (!tradingCmds[sub]) {
1338
- log(`Unknown trade subcommand: ${sub}`);
1339
- log(`Available: quote, execute`);
1340
- log(`Run 'nansen trade help' for usage.`);
1341
- return;
1203
+ throw new NansenError(`Unknown trade subcommand: ${sub}. Available: quote, execute`, ErrorCode.UNKNOWN);
1342
1204
  }
1343
1205
  return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
1344
1206
  };
@@ -1351,9 +1213,6 @@ export const DEPRECATED_TO_RESEARCH = new Set(['smart-money', 'profiler', 'token
1351
1213
  // Subcommands that moved under 'trade'
1352
1214
  export const DEPRECATED_TO_TRADE = new Set(['quote', 'execute']);
1353
1215
 
1354
- // Commands that don't require API authentication
1355
- export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'trade', 'quote', 'execute', 'changelog'];
1356
-
1357
1216
  // Command aliases: top-level shortcuts that resolve before routing
1358
1217
  export const COMMAND_ALIASES = {
1359
1218
  'tgm': 'token', // Token God Mode
@@ -1372,7 +1231,6 @@ export const RESEARCH_CATEGORY_ALIASES = {
1372
1231
 
1373
1232
  // Generate help text for a specific subcommand using SCHEMA
1374
1233
  export function generateSubcommandHelp(command, subcommand) {
1375
- // Look up in top-level commands, then fall back to research subcommands
1376
1234
  const cmdSchema = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
1377
1235
  if (!cmdSchema) return null;
1378
1236
 
@@ -1380,100 +1238,36 @@ export function generateSubcommandHelp(command, subcommand) {
1380
1238
  if (!subSchema) return null;
1381
1239
 
1382
1240
  const lines = [];
1383
- lines.push(`\n${command} ${subcommand} - ${subSchema.description || 'No description'}\n`);
1384
-
1385
- // Usage
1386
- const requiredOpts = [];
1387
- const optionalOpts = [];
1388
-
1241
+ lines.push(`${command} ${subcommand} ${subSchema.description || 'No description'}`);
1242
+
1389
1243
  if (subSchema.options) {
1390
- for (const [name, opt] of Object.entries(subSchema.options)) {
1391
- if (opt.required) {
1392
- requiredOpts.push(name);
1393
- } else {
1394
- optionalOpts.push(name);
1395
- }
1396
- }
1397
- }
1398
-
1399
- let usage = `USAGE:\n nansen ${command} ${subcommand}`;
1400
- if (requiredOpts.length) {
1401
- usage += ' ' + requiredOpts.map(o => `--${o} <value>`).join(' ');
1402
- }
1403
- if (optionalOpts.length) {
1404
- usage += ' [options]';
1405
- }
1406
- lines.push(usage);
1407
-
1408
- // Required options
1409
- if (requiredOpts.length) {
1410
- lines.push('\nREQUIRED:');
1411
- for (const name of requiredOpts) {
1412
- const opt = subSchema.options[name];
1413
- const desc = opt.description || `${opt.type}`;
1414
- lines.push(` --${name.padEnd(16)} ${desc}`);
1415
- }
1416
- }
1417
-
1418
- // Optional options
1419
- if (optionalOpts.length) {
1420
- lines.push('\nOPTIONS:');
1421
- for (const name of optionalOpts) {
1422
- const opt = subSchema.options[name];
1423
- const defaultStr = opt.default !== undefined ? ` (default: ${opt.default})` : '';
1424
- const desc = (opt.description || opt.type) + defaultStr;
1425
- lines.push(` --${name.padEnd(16)} ${desc}`);
1426
- }
1244
+ const params = Object.entries(subSchema.options).map(([name, opt]) => {
1245
+ const parts = [`--${name}`];
1246
+ if (opt.required) parts[0] += '*';
1247
+ if (opt.default !== undefined) parts.push(`(${opt.default})`);
1248
+ if (opt.enum) parts.push(`[${opt.enum.join('|')}]`);
1249
+ return parts.join(' ');
1250
+ });
1251
+ lines.push(`Params (* required): ${params.join(', ')}`);
1427
1252
  }
1428
-
1429
- // Return fields
1430
- if (subSchema.returns && subSchema.returns.length) {
1431
- lines.push('\nRETURNS:');
1432
- lines.push(` ${subSchema.returns.join(', ')}`);
1253
+
1254
+ if (subSchema.returns?.length) {
1255
+ lines.push(`Returns: ${subSchema.returns.join(', ')}`);
1433
1256
  }
1434
-
1435
- // Examples
1436
- lines.push('\nEXAMPLES:');
1257
+
1258
+ const exampleValues = { address: '0x...', token: '0x...', query: '"term"', symbol: 'BTC', date: '2024-01-01' };
1437
1259
  const chain = subSchema.options?.chain?.default || 'solana';
1438
-
1439
- // Example values for common required options
1440
- const exampleValues = {
1441
- address: '0x123...',
1442
- token: '0x123...',
1443
- query: '"search term"',
1444
- symbol: 'BTC',
1445
- date: '2024-01-01'
1446
- };
1447
-
1448
- // Build example based on required options
1449
- let example = ` nansen ${command} ${subcommand}`;
1450
- for (const name of requiredOpts) {
1451
- const value = exampleValues[name] || '<value>';
1452
- example += ` --${name} ${value}`;
1260
+ let example = `nansen ${command} ${subcommand}`;
1261
+ if (subSchema.options) {
1262
+ for (const [name, opt] of Object.entries(subSchema.options)) {
1263
+ if (opt.required) example += ` --${name} ${exampleValues[name] || '<val>'}`;
1264
+ }
1453
1265
  }
1454
- if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
1266
+ if (subSchema.options?.chain && !subSchema.options.chain.required) {
1455
1267
  example += ` --chain ${chain}`;
1456
1268
  }
1457
- example += ' --pretty';
1458
- lines.push(example);
1459
-
1460
- // Add a filtered example if filters are supported
1461
- if (subSchema.options?.filters || subSchema.options?.labels) {
1462
- let filterExample = ` nansen ${command} ${subcommand}`;
1463
- for (const name of requiredOpts) {
1464
- const value = exampleValues[name] || '<value>';
1465
- filterExample += ` --${name} ${value}`;
1466
- }
1467
- if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
1468
- filterExample += ` --chain ${chain}`;
1469
- }
1470
- if (subSchema.options?.labels) {
1471
- filterExample += ' --labels "Smart Trader"';
1472
- }
1473
- filterExample += ' --limit 10 --table';
1474
- lines.push(filterExample);
1475
- }
1476
-
1269
+ lines.push(`Example: ${example}`);
1270
+
1477
1271
  return lines.join('\n');
1478
1272
  }
1479
1273
 
@@ -1542,13 +1336,10 @@ export async function runCLI(rawArgs, deps = {}) {
1542
1336
  const researchCat = SCHEMA.commands.research.subcommands[category];
1543
1337
  if (researchCat) {
1544
1338
  const catSchema = researchCat;
1545
- const lines = [`\nresearch ${category} - ${catSchema.description}\n`];
1339
+ const lines = [`research ${category} ${catSchema.description}`];
1546
1340
  if (catSchema.subcommands) {
1547
- lines.push('SUBCOMMANDS:');
1548
- for (const [sub, subSchema] of Object.entries(catSchema.subcommands)) {
1549
- lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
1550
- }
1551
- lines.push(`\nFor detailed help: nansen research ${category} <subcommand> --help`);
1341
+ lines.push('Subcommands: ' + Object.keys(catSchema.subcommands).join(', '));
1342
+ lines.push(`Use: nansen research ${category} <subcommand> --help`);
1552
1343
  }
1553
1344
  output(lines.join('\n'));
1554
1345
  notify();
@@ -1577,13 +1368,10 @@ export async function runCLI(rawArgs, deps = {}) {
1577
1368
  const cmdSchemaLookup = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
1578
1369
  if (command && cmdSchemaLookup) {
1579
1370
  const cmdSchema = cmdSchemaLookup;
1580
- const lines = [`\n${command} - ${cmdSchema.description}\n`];
1371
+ const lines = [`${command} ${cmdSchema.description}`];
1581
1372
  if (cmdSchema.subcommands) {
1582
- lines.push('SUBCOMMANDS:');
1583
- for (const [sub, subSchema] of Object.entries(cmdSchema.subcommands)) {
1584
- lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
1585
- }
1586
- lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
1373
+ lines.push('Subcommands: ' + Object.keys(cmdSchema.subcommands).join(', '));
1374
+ lines.push(`Use: nansen ${command} <subcommand> --help`);
1587
1375
  }
1588
1376
  output(lines.join('\n'));
1589
1377
  notify();
@@ -1610,48 +1398,46 @@ export async function runCLI(rawArgs, deps = {}) {
1610
1398
  return { type: 'error', data: errorData };
1611
1399
  }
1612
1400
 
1613
- // Commands that don't require API authentication
1614
- if (NO_AUTH_COMMANDS.includes(command)) {
1615
- const result = await commands[command](subArgs, null, flags, options);
1616
-
1617
- // Schema command returns data that should be output
1618
- if (command === 'schema' && result) {
1619
- const formatted = formatOutput(result, { pretty, table: false });
1620
- output(formatted.text);
1621
- notify();
1622
- return { type: 'schema', data: result };
1623
- }
1624
-
1625
- notify();
1626
- return { type: 'no-auth', command };
1627
- }
1628
-
1629
1401
  try {
1630
1402
  // Configure retry options
1631
- const retryOptions = flags['no-retry']
1632
- ? { maxRetries: 0 }
1403
+ const retryOptions = flags['no-retry']
1404
+ ? { maxRetries: 0 }
1633
1405
  : { maxRetries: options.retries !== undefined ? (Number.isNaN(parseInt(options.retries, 10)) ? 3 : parseInt(options.retries, 10)) : 3 };
1634
-
1406
+
1635
1407
  // Configure cache options
1636
1408
  const cacheTtl = options['cache-ttl'] !== undefined ? parseInt(options['cache-ttl'], 10) : 300;
1637
1409
  const cacheOptions = {
1638
1410
  enabled: flags['cache'] && !flags['no-cache'],
1639
1411
  ttl: Number.isNaN(cacheTtl) ? 300 : cacheTtl
1640
1412
  };
1641
-
1413
+
1642
1414
  const defaultHeaders = {};
1643
1415
  if (options['x402-payment-signature']) {
1644
1416
  defaultHeaders['Payment-Signature'] = options['x402-payment-signature'];
1645
1417
  }
1646
1418
  const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions, defaultHeaders });
1647
1419
  let result = await commands[command](subArgs, api, flags, options);
1648
-
1420
+
1421
+ // Commands that handle their own output return undefined
1422
+ if (result === undefined) {
1423
+ notify();
1424
+ return { type: 'no-output', command };
1425
+ }
1426
+
1427
+ // Schema returns data directly (not wrapped in { success, data })
1428
+ if (command === 'schema') {
1429
+ const formatted = formatOutput(result, { pretty, table: false });
1430
+ output(formatted.text);
1431
+ notify();
1432
+ return { type: 'schema', data: result };
1433
+ }
1434
+
1649
1435
  // Apply field filtering if --fields is specified
1650
1436
  const fields = parseFields(options.fields);
1651
1437
  if (fields) {
1652
1438
  result = filterFields(result, fields);
1653
1439
  }
1654
-
1440
+
1655
1441
  // Output in requested format
1656
1442
  if (stream) {
1657
1443
  // Stream mode: output each record as a JSON line (NDJSON)