nansen-cli 1.7.0 → 1.9.0

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
@@ -6,8 +6,9 @@
6
6
  import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, sleep } from './api.js';
7
7
  import { buildWalletCommands } from './wallet.js';
8
8
  import { buildTradingCommands } from './trading.js';
9
+ import { resolveAddress, isEnsName } from './ens.js';
9
10
  import fs from 'fs';
10
- import { getUpdateNotification, scheduleUpdateCheck } from './update-check.js';
11
+ import { getUpdateNotification, getUpgradeNotice, scheduleUpdateCheck } from './update-check.js';
11
12
  import { createRequire } from 'module';
12
13
  import * as readline from 'readline';
13
14
 
@@ -19,296 +20,140 @@ const { version: VERSION } = require('../package.json');
19
20
  export const SCHEMA = {
20
21
  version: VERSION,
21
22
  commands: {
22
- 'smart-money': {
23
- description: 'Smart Money analytics - track sophisticated market participants',
23
+ 'research': {
24
+ description: 'Research and analytics commands',
24
25
  subcommands: {
25
- 'netflow': {
26
- description: 'Net capital flows (inflows vs outflows)',
27
- options: {
28
- chain: { type: 'string', default: 'solana', description: 'Blockchain to query' },
29
- chains: { type: 'array', description: 'Multiple chains as JSON array' },
30
- limit: { type: 'number', description: 'Number of results' },
31
- labels: { type: 'string|array', description: 'Smart Money label filter' },
32
- sort: { type: 'string', description: 'Sort field:direction (e.g., value_usd:desc)' },
33
- filters: { type: 'object', description: 'Additional filters as JSON' }
34
- },
35
- returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'inflow_usd', 'outflow_usd', 'net_flow_usd']
36
- },
37
- 'dex-trades': {
38
- description: 'Real-time DEX trading activity',
39
- options: {
40
- chain: { type: 'string', default: 'solana' },
41
- chains: { type: 'array' },
42
- limit: { type: 'number' },
43
- labels: { type: 'string|array' },
44
- sort: { type: 'string' },
45
- filters: { type: 'object' }
46
- },
47
- 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']
48
- },
49
- 'perp-trades': {
50
- description: 'Perpetual trading on Hyperliquid',
51
- options: { limit: { type: 'number' }, sort: { type: 'string' }, filters: { type: 'object' } },
52
- returns: ['trader_address', 'trader_address_label', 'token_symbol', 'side', 'action', 'token_amount', 'price_usd', 'value_usd', 'type', 'block_timestamp', 'transaction_hash']
53
- },
54
- 'holdings': {
55
- description: 'Aggregated token balances',
56
- options: { chain: { type: 'string', default: 'solana' }, chains: { type: 'array' }, limit: { type: 'number' }, labels: { type: 'string|array' } },
57
- 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']
58
- },
59
- 'dcas': {
60
- description: 'DCA strategies on Jupiter',
61
- options: { limit: { type: 'number' }, filters: { type: 'object' } },
62
- 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']
63
- },
64
- 'historical-holdings': {
65
- description: 'Historical holdings over time',
66
- options: { chain: { type: 'string', default: 'solana' }, chains: { type: 'array' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
67
- returns: ['date', 'token_address', 'token_symbol', 'balance', 'balance_usd']
68
- }
69
- }
70
- },
71
- 'profiler': {
72
- description: 'Wallet profiling - detailed information about any blockchain address',
73
- subcommands: {
74
- 'balance': {
75
- description: 'Current token holdings',
76
- options: {
77
- address: { type: 'string', required: true, description: 'Wallet address to query' },
78
- chain: { type: 'string', default: 'ethereum' },
79
- entity: { type: 'string', description: 'Entity name instead of address' }
80
- },
81
- returns: ['chain', 'address', 'token_address', 'token_symbol', 'token_name', 'token_amount', 'price_usd', 'value_usd']
82
- },
83
- 'labels': {
84
- description: 'Behavioral and entity labels',
85
- options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' } },
86
- returns: ['label', 'label_type', 'label_subtype']
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
+ }
87
67
  },
88
- 'transactions': {
89
- description: 'Transaction history',
90
- options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, date: { type: 'string', required: true, description: 'Date or date range (YYYY-MM-DD or {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"})' }, limit: { type: 'number' }, days: { type: 'number', default: 30 } },
91
- returns: ['chain', 'method', 'tokens_sent', 'tokens_received', 'volume_usd', 'block_timestamp', 'transaction_hash']
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
+ }
92
86
  },
93
- 'pnl': {
94
- description: 'PnL and trade performance',
95
- options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, date: { type: 'string', description: 'Date or date range (YYYY-MM-DD or {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"})' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
96
- returns: ['token_address', 'token_symbol', 'realized_pnl_usd', 'unrealized_pnl_usd', 'total_pnl_usd']
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
+ }
97
106
  },
98
107
  'search': {
99
- description: 'Search for entities by name',
100
- options: { query: { type: 'string', required: true, description: 'Search query' }, limit: { type: 'number' } },
101
- returns: ['entity_name']
102
- },
103
- 'historical-balances': {
104
- description: 'Historical balances over time',
105
- options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, days: { type: 'number', default: 30 } },
106
- returns: ['date', 'token_address', 'token_symbol', 'balance', 'balance_usd']
107
- },
108
- 'related-wallets': {
109
- description: 'Find wallets related to an address',
110
- options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, limit: { type: 'number' } },
111
- returns: ['address', 'address_label', 'relation', 'transaction_hash', 'block_timestamp', 'order', 'chain']
112
- },
113
- 'counterparties': {
114
- description: 'Top counterparties by volume',
115
- options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, days: { type: 'number', default: 30 } },
116
- returns: ['counterparty_address', 'counterparty_address_label', 'interaction_count', 'total_volume_usd', 'volume_in_usd', 'volume_out_usd', 'tokens_info']
117
- },
118
- 'pnl-summary': {
119
- description: 'Summarized PnL metrics',
120
- options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, days: { type: 'number', default: 30 } },
121
- returns: ['top5_tokens', 'traded_token_count', 'traded_times', 'realized_pnl_usd', 'realized_pnl_percent', 'win_rate']
122
- },
123
- 'perp-positions': {
124
- description: 'Current perpetual positions',
125
- options: { address: { type: 'string', required: true }, limit: { type: 'number' } },
126
- returns: ['symbol', 'side', 'size', 'entry_price', 'mark_price', 'unrealized_pnl', 'leverage']
127
- },
128
- 'perp-trades': {
129
- description: 'Perpetual trading history',
130
- options: { address: { type: 'string', required: true }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
131
- returns: ['symbol', 'side', 'size', 'price', 'value_usd', 'pnl_usd', 'timestamp']
132
- },
133
- 'batch': {
134
- description: 'Batch profile multiple addresses',
135
- options: {
136
- addresses: { type: 'string', description: 'Comma-separated addresses' },
137
- file: { type: 'string', description: 'File with one address per line' },
138
- chain: { type: 'string', default: 'ethereum' },
139
- include: { type: 'string', default: 'labels,balance', description: 'Comma-separated: labels,balance,pnl' },
140
- delay: { type: 'number', default: 1000, description: 'Delay between requests in ms' }
141
- },
142
- returns: ['address', 'chain', 'labels', 'balance', 'pnl', 'error']
143
- },
144
- 'trace': {
145
- description: 'Multi-hop counterparty trace (BFS)',
146
- options: {
147
- address: { type: 'string', required: true },
148
- chain: { type: 'string', default: 'ethereum' },
149
- depth: { type: 'number', default: 2, description: 'Max hops (1-5)' },
150
- width: { type: 'number', default: 10, description: 'Top N counterparties per hop' },
151
- days: { type: 'number', default: 30 },
152
- delay: { type: 'number', default: 1000, description: 'Delay between requests in ms' }
153
- },
154
- returns: ['root', 'chain', 'depth', 'nodes', 'edges', 'stats']
155
- },
156
- 'compare': {
157
- description: 'Compare two wallets (shared counterparties, tokens)',
158
- options: {
159
- addresses: { type: 'string', required: true, description: 'Two comma-separated addresses' },
160
- chain: { type: 'string', default: 'ethereum' },
161
- days: { type: 'number', default: 30 }
162
- },
163
- returns: ['addresses', 'chain', 'shared_counterparties', 'shared_tokens', 'balances']
164
- }
165
- }
166
- },
167
- 'token': {
168
- description: 'Token God Mode - deep analytics for any token',
169
- subcommands: {
170
- 'indicators': {
171
- description: 'Risk and reward indicators for a token (Nansen Score)',
108
+ description: 'Search for tokens and entities across Nansen',
172
109
  options: {
173
- token: { type: 'string', required: true, description: 'Token address' },
174
- chain: { type: 'string', default: 'ethereum' }
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)' }
175
114
  },
176
- 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]']
115
+ returns: ['tokens[name, symbol, chain, address, price, volume_24h, market_cap, rank]', 'entities[name, tags, rank]', 'total_results']
177
116
  },
178
- 'info': {
179
- description: 'Get detailed information for a specific token',
180
- options: {
181
- token: { type: 'string', required: true, description: 'Token address' },
182
- chain: { type: 'string', default: 'solana' },
183
- timeframe: { type: 'string', default: '1d', enum: ['5m', '1h', '6h', '12h', '1d', '7d'] }
184
- },
185
- returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd']
186
- },
187
- 'screener': {
188
- description: 'Discover and filter tokens',
189
- options: {
190
- chain: { type: 'string', default: 'solana' },
191
- chains: { type: 'array' },
192
- timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] },
193
- 'smart-money': { type: 'boolean', description: 'Filter for Smart Money only' },
194
- search: { type: 'string', description: 'Filter results by token symbol or name (client-side)' },
195
- limit: { type: 'number' },
196
- sort: { type: 'string' }
197
- },
198
- returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'smart_money_holders']
199
- },
200
- 'holders': {
201
- description: 'Token holder analysis',
202
- options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, 'smart-money': { type: 'boolean' }, limit: { type: 'number' } },
203
- returns: ['address', 'address_label', 'token_amount', 'total_outflow', 'total_inflow', 'balance_change_24h', 'balance_change_7d', 'balance_change_30d', 'ownership_percentage', 'value_usd']
204
- },
205
- 'flows': {
206
- description: 'Token flow metrics',
207
- options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, date: { type: 'string', required: true, description: 'Date or date range (YYYY-MM-DD or {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"})' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
208
- returns: ['date', 'price_usd', 'token_amount', 'value_usd', 'holders_count', 'total_inflows_count', 'total_outflows_count']
209
- },
210
- 'dex-trades': {
211
- description: 'DEX trading activity',
212
- options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, 'smart-money': { type: 'boolean' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
213
- returns: ['tx_hash', 'wallet_address', 'side', 'amount', 'price_usd', 'value_usd', 'timestamp']
214
- },
215
- 'pnl': {
216
- description: 'PnL leaderboard',
217
- options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, days: { type: 'number', default: 30 }, limit: { type: 'number' }, sort: { type: 'string' } },
218
- returns: ['wallet_address', 'realized_pnl_usd', 'unrealized_pnl_usd', 'total_pnl_usd', 'labels']
219
- },
220
- 'who-bought-sold': {
221
- description: 'Recent buyers and sellers',
222
- options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, date: { type: 'string', required: true, description: 'Date or date range (YYYY-MM-DD or {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"})' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
223
- returns: ['address', 'address_label', 'bought_token_volume', 'sold_token_volume', 'token_trade_volume', 'bought_volume_usd', 'sold_volume_usd', 'trade_volume_usd']
224
- },
225
- 'flow-intelligence': {
226
- description: 'Detailed flow intelligence by label',
227
- options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, days: { type: 'number', default: 30 } },
228
- 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']
229
- },
230
- 'transfers': {
231
- description: 'Token transfer history',
232
- 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' } },
233
- returns: ['tx_hash', 'from', 'to', 'amount', 'value_usd', 'timestamp']
234
- },
235
- 'jup-dca': {
236
- description: 'Jupiter DCA orders for token',
237
- options: { token: { type: 'string', required: true }, limit: { type: 'number' } },
238
- returns: ['wallet_address', 'input_token', 'output_token', 'total_input', 'executed', 'remaining']
239
- },
240
- 'perp-trades': {
241
- description: 'Perp trades by token symbol',
242
- options: { symbol: { type: 'string', required: true, description: 'Token symbol (e.g., BTC, ETH)' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
243
- returns: ['wallet_address', 'side', 'size', 'price', 'value_usd', 'pnl_usd', 'timestamp']
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
+ }
244
123
  },
245
- 'perp-positions': {
246
- description: 'Open perp positions by token symbol',
247
- options: { symbol: { type: 'string', required: true }, limit: { type: 'number' } },
248
- returns: ['wallet_address', 'side', 'size', 'entry_price', 'mark_price', 'unrealized_pnl', 'leverage']
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
+ }
249
129
  },
250
- 'perp-pnl-leaderboard': {
251
- description: 'Perp PnL leaderboard by token',
252
- options: { symbol: { type: 'string', required: true }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
253
- returns: ['wallet_address', 'realized_pnl', 'unrealized_pnl', 'total_pnl', 'trade_count']
254
- }
255
- }
256
- },
257
- 'portfolio': {
258
- description: 'Portfolio analytics',
259
- subcommands: {
260
- 'defi': {
261
- description: 'DeFi holdings across protocols',
262
- options: { wallet: { type: 'string', required: true, description: 'Wallet address' } },
263
- returns: ['protocol', 'chain', 'position_type', 'token_symbol', 'balance', 'balance_usd']
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
+ }
264
135
  }
265
136
  }
266
137
  },
267
- 'perp': {
268
- description: 'Perpetual futures analytics',
138
+ 'trade': {
139
+ description: 'DEX trading commands',
269
140
  subcommands: {
270
- 'screener': {
271
- description: 'Screen perpetual futures contracts',
141
+ 'quote': {
142
+ description: 'Get a DEX swap quote (chain, tokens, amount)',
272
143
  options: {
273
- days: { type: 'number', default: 30 },
274
- limit: { type: 'number' },
275
- sort: { type: 'string' },
276
- filters: { type: 'object' }
277
- },
278
- returns: ['token_symbol', 'volume_usd', 'open_interest', 'funding_rate', 'price_change_24h']
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
+ }
279
150
  },
280
- 'leaderboard': {
281
- description: 'Perpetual futures PnL leaderboard',
282
- options: {
283
- days: { type: 'number', default: 30 },
284
- limit: { type: 'number' },
285
- sort: { type: 'string' },
286
- filters: { type: 'object' }
287
- },
288
- returns: ['address', 'address_label', 'realized_pnl', 'unrealized_pnl', 'total_pnl', 'trade_count', 'win_rate']
289
- }
290
- }
291
- },
292
- 'search': {
293
- description: 'Search for tokens and entities across Nansen',
294
- options: {
295
- query: { type: 'string', required: true, description: 'Search query (token name, symbol, address, or entity)' },
296
- type: { type: 'string', default: 'any', enum: ['token', 'entity', 'any'], description: 'Result type filter' },
297
- chain: { type: 'string', description: 'Filter by chain (e.g., ethereum, solana)' },
298
- limit: { type: 'number', default: 25, description: 'Max results (1-50)' }
299
- },
300
- returns: ['tokens[name, symbol, chain, address, price, volume_24h, market_cap, rank]', 'entities[name, tags, rank]', 'total_results']
301
- },
302
- 'points': {
303
- description: 'Nansen Points analytics',
304
- subcommands: {
305
- 'leaderboard': {
306
- description: 'Points leaderboard',
151
+ 'execute': {
152
+ description: 'Sign and broadcast a quoted trade',
307
153
  options: {
308
- tier: { type: 'string', description: 'Filter by tier' },
309
- limit: { type: 'number' }
310
- },
311
- returns: ['rank', 'address', 'address_label', 'points', 'tier']
154
+ chain: { type: 'string', default: 'ethereum', description: 'Blockchain' },
155
+ wallet: { type: 'string', description: 'Wallet name, or "walletconnect"/"wc" for WalletConnect (EVM only)' }
156
+ }
312
157
  }
313
158
  }
314
159
  }
@@ -376,6 +221,19 @@ export function parseFields(fieldsOption) {
376
221
  }
377
222
 
378
223
  // Parse command line arguments
224
+ /**
225
+ * Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
226
+ */
227
+ function compareSemver(a, b) {
228
+ const parse = v => v.replace(/^v/, '').split('.').map(Number);
229
+ const [aM, am, ap] = parse(a);
230
+ const [bM, bm, bp] = parse(b);
231
+ if (aM !== bM) return aM > bM ? 1 : -1;
232
+ if (am !== bm) return am > bm ? 1 : -1;
233
+ if (ap !== bp) return ap > bp ? 1 : -1;
234
+ return 0;
235
+ }
236
+
379
237
  export function parseArgs(args) {
380
238
  const result = { _: [], flags: {}, options: {} };
381
239
 
@@ -389,9 +247,12 @@ export function parseArgs(args) {
389
247
  if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich') {
390
248
  result.flags[key] = true;
391
249
  } else if (next && !next.startsWith('-')) {
392
- // Try to parse as JSON first
250
+ // Try to parse as JSON first (for objects/arrays/booleans),
251
+ // but keep numeric strings as strings to avoid precision loss
252
+ // and scientific notation for large integers (e.g. 1e+21).
393
253
  try {
394
- result.options[key] = JSON.parse(next);
254
+ const parsed = JSON.parse(next);
255
+ result.options[key] = typeof parsed === 'number' ? next : parsed;
395
256
  } catch {
396
257
  result.options[key] = next;
397
258
  }
@@ -672,8 +533,24 @@ export async function batchProfile(api, params = {}) {
672
533
  const { addresses = [], chain = 'ethereum', include = ['labels', 'balance'], delayMs = 1000 } = params;
673
534
  const results = [];
674
535
  for (let i = 0; i < addresses.length; i++) {
675
- const address = addresses[i].trim();
536
+ let address = addresses[i].trim();
676
537
  const entry = { address, chain };
538
+
539
+ // Resolve ENS names
540
+ if (isEnsName(address)) {
541
+ try {
542
+ const resolved = await resolveAddress(address, chain);
543
+ entry.ensName = resolved.ensName;
544
+ address = resolved.address;
545
+ entry.address = address;
546
+ } catch (err) {
547
+ entry.error = err.message;
548
+ results.push(entry);
549
+ if (i < addresses.length - 1) await sleep(delayMs);
550
+ continue;
551
+ }
552
+ }
553
+
677
554
  const validation = validateAddress(address, chain);
678
555
  if (!validation.valid) {
679
556
  entry.error = validation.error;
@@ -701,10 +578,21 @@ export async function batchProfile(api, params = {}) {
701
578
  }
702
579
 
703
580
  export async function traceCounterparties(api, params = {}) {
704
- const { address, chain = 'ethereum', depth = 2, width = 10, days = 30, delayMs = 1000 } = params;
581
+ let { address, chain = 'ethereum', depth = 2, width = 10, days = 30, delayMs = 1000 } = params;
705
582
  if (!address) {
706
583
  throw new NansenError('address is required for trace', ErrorCode.MISSING_PARAM);
707
584
  }
585
+
586
+ // Resolve ENS names
587
+ if (isEnsName(address)) {
588
+ try {
589
+ const resolved = await resolveAddress(address, chain);
590
+ address = resolved.address;
591
+ } catch (err) {
592
+ throw new NansenError(err.message, ErrorCode.INVALID_ADDRESS);
593
+ }
594
+ }
595
+
708
596
  const validation = validateAddress(address, chain);
709
597
  if (!validation.valid) {
710
598
  throw new NansenError(validation.error, ErrorCode.INVALID_ADDRESS);
@@ -838,23 +726,13 @@ USAGE:
838
726
  nansen <command> [subcommand] [options]
839
727
 
840
728
  COMMANDS:
841
- login Save your API key (interactive)
842
- logout Remove saved API key
843
- schema Output JSON schema for all commands (for agent introspection)
729
+ research Research & analytics (smart-money, profiler, token, search, perp, portfolio, points)
730
+ trade DEX trading (quote, execute)
844
731
  wallet Local wallet management (create, list, show, export, default, delete)
845
- quote Get a DEX swap quote (chain, tokens, amount)
846
- execute Sign and broadcast a quoted trade
732
+ login/logout API key management
733
+ schema Output JSON schema for all commands (for agent introspection)
847
734
  cache Cache management (clear)
848
- smart-money Smart Money analytics (netflow, dex-trades, perp-trades, holdings, dcas, historical-holdings)
849
- profiler Wallet profiling (balance, labels, transactions, pnl, pnl-summary, search,
850
- historical-balances, related-wallets, counterparties, perp-positions, perp-trades,
851
- batch, trace, compare)
852
- token Token God Mode (info, indicators, screener, holders, flows, dex-trades, pnl,
853
- who-bought-sold, flow-intelligence, transfers, jup-dca, perp-trades,
854
- perp-positions, perp-pnl-leaderboard)
855
- portfolio Portfolio analytics (defi)
856
- perp Perpetual futures analytics (screener, leaderboard)
857
- points Nansen Points analytics (leaderboard)
735
+ changelog Show what's new (use --since <version> to filter)
858
736
  help Show this help message
859
737
 
860
738
  GLOBAL OPTIONS:
@@ -880,22 +758,19 @@ GLOBAL OPTIONS:
880
758
 
881
759
  EXAMPLES:
882
760
  # Get Smart Money netflow on Solana
883
- nansen smart-money netflow --chain solana
761
+ nansen research smart-money netflow --chain solana
884
762
 
885
763
  # Get top tokens by Smart Money activity
886
- nansen token screener --chain solana --timeframe 24h --pretty
764
+ nansen research token screener --chain solana --timeframe 24h --pretty
887
765
 
888
766
  # Get wallet balance
889
- nansen profiler balance --address 0x123... --chain ethereum
890
-
891
- # Get wallet labels
892
- nansen profiler labels --address 0x123... --chain ethereum
767
+ nansen research profiler balance --address 0x123... --chain ethereum
893
768
 
894
- # Search for entity
895
- nansen profiler search --query "Vitalik"
769
+ # Search for tokens/entities
770
+ nansen research search "Vitalik"
896
771
 
897
- # Get token holders with filters
898
- nansen token holders --token 0x123... --smart-money
772
+ # Get a DEX swap quote
773
+ nansen trade quote --chain ethereum --from ETH --to USDC --amount 1
899
774
 
900
775
  SMART MONEY LABELS:
901
776
  Fund, Smart Trader, 30D Smart Trader, 90D Smart Trader,
@@ -964,52 +839,53 @@ export function buildCommands(deps = {}) {
964
839
  api = null,
965
840
  promptFn = prompt,
966
841
  log = console.log,
842
+ errorOutput = console.error,
967
843
  NansenAPIClass = NansenAPI,
968
844
  saveConfigFn = saveConfig,
969
845
  deleteConfigFn = deleteConfig,
970
846
  getConfigFileFn = getConfigFile,
971
- exit = process.exit
847
+ exit = process.exit,
848
+ isTTY = process.stdin.isTTY
972
849
  } = deps;
973
850
 
974
- return {
851
+ const cmds = {
975
852
  'login': async (args, apiInstance, flags, options) => {
976
853
  // Support non-interactive: nansen login --api-key <key>
977
854
  let apiKey = options['api-key'] || options.apiKey;
978
855
 
979
856
  if (!apiKey) {
980
- log('Nansen CLI Login\n');
981
- log('Get your API key at: https://app.nansen.ai/api\n');
982
- log('Tip: For non-interactive use: nansen login --api-key <key>\n');
983
-
984
- apiKey = await promptFn('Enter your API key: ', true);
857
+ if (!isTTY) {
858
+ // Non-interactive mode: check env var fallback
859
+ apiKey = process.env.NANSEN_API_KEY;
860
+ if (!apiKey) {
861
+ log(' No API key provided. Use: nansen login --api-key <key>\n Or set NANSEN_API_KEY environment variable.');
862
+ exit(1);
863
+ return;
864
+ }
865
+ } else {
866
+ log('Nansen CLI Login\n');
867
+ log('Get your API key at: https://app.nansen.ai/api\n');
868
+ log('Tip: For non-interactive use: nansen login --api-key <key>\n');
869
+
870
+ apiKey = await promptFn('Enter your API key: ', true);
871
+ }
985
872
  }
986
-
873
+
987
874
  if (!apiKey || apiKey.trim().length === 0) {
988
875
  log('\n❌ No API key provided');
989
876
  exit(1);
990
877
  return;
991
878
  }
992
-
993
- // Validate the key with a test request
994
- log('\nValidating API key...');
995
- try {
996
- const testApi = new NansenAPIClass(apiKey.trim());
997
- await testApi.tokenScreener({ chains: ['solana'], pagination: { page: 1, per_page: 1 } });
998
-
999
- // Save the config
1000
- saveConfigFn({
1001
- apiKey: apiKey.trim(),
1002
- baseUrl: 'https://api.nansen.ai'
1003
- });
1004
-
1005
- log('✓ API key validated');
1006
- log(`✓ Saved to ${getConfigFileFn()}\n`);
1007
- log('You can now use the Nansen CLI. Try:');
1008
- log(' nansen token screener --chain solana --pretty');
1009
- } catch (error) {
1010
- log(`\n❌ Invalid API key: ${error.message}`);
1011
- exit(1);
1012
- }
879
+
880
+ // Save the config without validation
881
+ saveConfigFn({
882
+ apiKey: apiKey.trim(),
883
+ baseUrl: 'https://api.nansen.ai'
884
+ });
885
+
886
+ log(`✓ Saved to ${getConfigFileFn()}\n`);
887
+ log('You can now use the Nansen CLI. Try:');
888
+ log(' nansen token screener --chain solana --pretty');
1013
889
  },
1014
890
 
1015
891
  'logout': async (args, apiInstance, flags, options) => {
@@ -1025,21 +901,56 @@ export function buildCommands(deps = {}) {
1025
901
  log(HELP);
1026
902
  },
1027
903
 
904
+ 'changelog': async (args, apiInstance, flags, options) => {
905
+ const changelogPath = new URL('../CHANGELOG.md', import.meta.url).pathname;
906
+ let content;
907
+ try {
908
+ content = fs.readFileSync(changelogPath, 'utf8');
909
+ } catch {
910
+ log('CHANGELOG.md not found. Visit https://github.com/nansen-ai/nansen-cli/blob/main/CHANGELOG.md');
911
+ return;
912
+ }
913
+ const since = options.since;
914
+ if (since) {
915
+ // Show only entries from the given version onwards
916
+ const lines = content.split('\n');
917
+ const filtered = [];
918
+ let include = false;
919
+ for (const line of lines) {
920
+ // Match ## [x.y.z] headers
921
+ const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
922
+ if (match) {
923
+ const ver = match[1];
924
+ // Compare: include versions >= since, stop at versions < since
925
+ if (compareSemver(ver, since) >= 0) {
926
+ include = true;
927
+ } else {
928
+ include = false;
929
+ }
930
+ }
931
+ if (include) filtered.push(line);
932
+ }
933
+ log(filtered.join('\n') || `No changelog entries found for versions >= ${since}`);
934
+ } else {
935
+ log(content);
936
+ }
937
+ },
938
+
1028
939
  'schema': async (args, apiInstance, flags, options) => {
1029
940
  // Return schema for agent introspection
1030
941
  const subcommand = args[0];
1031
-
1032
- if (subcommand && SCHEMA.commands[subcommand]) {
1033
- // Return schema for specific command
942
+ const schemaEntry = subcommand && (SCHEMA.commands[subcommand] || SCHEMA.commands.research.subcommands[subcommand]);
943
+
944
+ if (schemaEntry) {
1034
945
  return {
1035
946
  command: subcommand,
1036
- ...SCHEMA.commands[subcommand],
947
+ ...schemaEntry,
1037
948
  globalOptions: SCHEMA.globalOptions,
1038
949
  chains: SCHEMA.chains,
1039
950
  smartMoneyLabels: SCHEMA.smartMoneyLabels
1040
951
  };
1041
952
  }
1042
-
953
+
1043
954
  // Return full schema
1044
955
  return SCHEMA;
1045
956
  },
@@ -1113,9 +1024,21 @@ export function buildCommands(deps = {}) {
1113
1024
 
1114
1025
  'profiler': async (args, apiInstance, flags, options) => {
1115
1026
  const subcommand = args[0] || 'help';
1116
- const address = options.address;
1027
+ let address = options.address;
1117
1028
  const entityName = options.entity || options['entity-name'];
1118
1029
  const chain = options.chain || 'ethereum';
1030
+
1031
+ // Resolve ENS names (e.g. vitalik.eth → 0x...)
1032
+ let ensName;
1033
+ if (address && isEnsName(address)) {
1034
+ try {
1035
+ const resolved = await resolveAddress(address, chain);
1036
+ address = resolved.address;
1037
+ ensName = resolved.ensName;
1038
+ } catch (err) {
1039
+ throw new NansenError(err.message, ErrorCode.INVALID_ADDRESS);
1040
+ }
1041
+ }
1119
1042
  const filters = options.filters || {};
1120
1043
  const orderBy = parseSort(options.sort, options['order-by']);
1121
1044
  const pagination = options.limit ? { page: 1, recordsPerPage: options.limit } : undefined;
@@ -1187,7 +1110,12 @@ export function buildCommands(deps = {}) {
1187
1110
  return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
1188
1111
  }
1189
1112
 
1190
- return handlers[subcommand]();
1113
+ const result = await handlers[subcommand]();
1114
+
1115
+ // Attach ENS metadata so the caller knows the name was resolved
1116
+ return ensName && result && typeof result === 'object'
1117
+ ? { ...result, _ens: { name: ensName, resolvedAddress: address } }
1118
+ : result;
1191
1119
  },
1192
1120
 
1193
1121
  'token': async (args, apiInstance, flags, options) => {
@@ -1211,6 +1139,7 @@ export function buildCommands(deps = {}) {
1211
1139
 
1212
1140
  const handlers = {
1213
1141
  'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
1142
+ 'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d', pagination }),
1214
1143
  'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
1215
1144
  'screener': async () => {
1216
1145
  const search = options.search;
@@ -1259,7 +1188,7 @@ export function buildCommands(deps = {}) {
1259
1188
  'perp-positions': () => apiInstance.tokenPerpPositions({ tokenSymbol, filters, orderBy, pagination }),
1260
1189
  'perp-pnl-leaderboard': () => apiInstance.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days }),
1261
1190
  'help': () => ({
1262
- commands: ['info', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
1191
+ commands: ['info', 'ohlcv', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
1263
1192
  description: 'Token God Mode endpoints',
1264
1193
  example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
1265
1194
  })
@@ -1354,12 +1283,73 @@ export function buildCommands(deps = {}) {
1354
1283
  return handlers[subcommand]();
1355
1284
  }
1356
1285
  };
1286
+
1287
+ // 'research' delegates to the category handlers defined above
1288
+ const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points']);
1289
+
1290
+ cmds['research'] = async (args, apiInstance, flags, options) => {
1291
+ const rawCategory = args[0];
1292
+ if (!rawCategory || rawCategory === 'help') {
1293
+ return {
1294
+ categories: [...RESEARCH_CATEGORIES],
1295
+ aliases: RESEARCH_CATEGORY_ALIASES,
1296
+ description: 'Research and analytics commands',
1297
+ example: 'nansen research smart-money netflow --chain solana'
1298
+ };
1299
+ }
1300
+ const category = RESEARCH_CATEGORY_ALIASES[rawCategory] || rawCategory;
1301
+ if (!RESEARCH_CATEGORIES.has(category)) {
1302
+ return { error: `Unknown research category: ${rawCategory}`, available: [...RESEARCH_CATEGORIES] };
1303
+ }
1304
+ return cmds[category](args.slice(1), apiInstance, flags, options);
1305
+ };
1306
+
1307
+ // 'trade' delegates to quote/execute from buildTradingCommands
1308
+ const tradingCmds = buildTradingCommands(deps);
1309
+ cmds['trade'] = async (args, apiInstance, flags, options) => {
1310
+ const sub = args[0];
1311
+ if (!sub || sub === 'help') {
1312
+ log(`nansen trade — DEX trading commands
1313
+
1314
+ SUBCOMMANDS:
1315
+ quote Get a swap quote (price, route, fees)
1316
+ execute Sign and broadcast a quoted swap
1317
+
1318
+ USAGE:
1319
+ nansen trade quote --chain <chain> --from <token> --to <token> --amount <units>
1320
+ nansen trade execute --quote <quoteId>
1321
+
1322
+ EXAMPLES:
1323
+ nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
1324
+ nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
1325
+ nansen trade execute --quote 1708900000000-abc123
1326
+
1327
+ SYMBOLS:
1328
+ Common tokens resolve automatically: SOL, ETH, BNB, USDC, USDT, WETH, WBNB
1329
+ Raw addresses are also accepted.`);
1330
+ return;
1331
+ }
1332
+ if (!tradingCmds[sub]) {
1333
+ log(`Unknown trade subcommand: ${sub}`);
1334
+ log(`Available: quote, execute`);
1335
+ log(`Run 'nansen trade help' for usage.`);
1336
+ return;
1337
+ }
1338
+ return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
1339
+ };
1340
+
1341
+ return cmds;
1357
1342
  }
1358
1343
 
1344
+ // Categories that moved under 'research'
1345
+ export const DEPRECATED_TO_RESEARCH = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points']);
1346
+ // Subcommands that moved under 'trade'
1347
+ export const DEPRECATED_TO_TRADE = new Set(['quote', 'execute']);
1348
+
1359
1349
  // Commands that don't require API authentication
1360
- export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'quote', 'execute'];
1350
+ export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'trade', 'quote', 'execute', 'changelog'];
1361
1351
 
1362
- // Command aliases for convenience
1352
+ // Command aliases: top-level shortcuts that resolve before routing
1363
1353
  export const COMMAND_ALIASES = {
1364
1354
  'tgm': 'token', // Token God Mode
1365
1355
  'sm': 'smart-money', // Smart Money
@@ -1367,11 +1357,20 @@ export const COMMAND_ALIASES = {
1367
1357
  'port': 'portfolio' // Portfolio
1368
1358
  };
1369
1359
 
1360
+ // Aliases used inside the 'research' namespace
1361
+ export const RESEARCH_CATEGORY_ALIASES = {
1362
+ 'tgm': 'token',
1363
+ 'sm': 'smart-money',
1364
+ 'prof': 'profiler',
1365
+ 'port': 'portfolio'
1366
+ };
1367
+
1370
1368
  // Generate help text for a specific subcommand using SCHEMA
1371
1369
  export function generateSubcommandHelp(command, subcommand) {
1372
- const cmdSchema = SCHEMA.commands[command];
1370
+ // Look up in top-level commands, then fall back to research subcommands
1371
+ const cmdSchema = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
1373
1372
  if (!cmdSchema) return null;
1374
-
1373
+
1375
1374
  const subSchema = cmdSchema.subcommands?.[subcommand];
1376
1375
  if (!subSchema) return null;
1377
1376
 
@@ -1490,6 +1489,14 @@ export async function runCLI(rawArgs, deps = {}) {
1490
1489
  const command = COMMAND_ALIASES[rawCommand] || rawCommand;
1491
1490
  const subArgs = positional.slice(1);
1492
1491
  const subcommand = subArgs[0];
1492
+
1493
+ // Deprecation warnings for commands that moved under 'research' or 'trade'
1494
+ if (DEPRECATED_TO_RESEARCH.has(command)) {
1495
+ errorOutput(`Warning: "nansen ${command}" is deprecated. Use "nansen research ${command}" instead.`);
1496
+ } else if (DEPRECATED_TO_TRADE.has(command)) {
1497
+ errorOutput(`Warning: "nansen ${command}" is deprecated. Use "nansen trade ${command}" instead.`);
1498
+ }
1499
+
1493
1500
  const pretty = flags.pretty || flags.p;
1494
1501
  const table = flags.table || flags.t;
1495
1502
  const stream = flags.stream || flags.s;
@@ -1497,8 +1504,12 @@ export async function runCLI(rawArgs, deps = {}) {
1497
1504
 
1498
1505
  // Update check (read cached result + schedule background refresh)
1499
1506
  const updateNotification = getUpdateNotification(VERSION);
1507
+ const upgradeNotice = getUpgradeNotice(VERSION);
1500
1508
  scheduleUpdateCheck();
1501
- const notify = () => { if (updateNotification) errorOutput(updateNotification); };
1509
+ const notify = () => {
1510
+ if (upgradeNotice) errorOutput(upgradeNotice);
1511
+ if (updateNotification) errorOutput(updateNotification);
1512
+ };
1502
1513
 
1503
1514
  const commands = { ...buildCommands(deps), ...buildWalletCommands(deps), ...buildTradingCommands(deps), ...commandOverrides };
1504
1515
 
@@ -1510,6 +1521,44 @@ export async function runCLI(rawArgs, deps = {}) {
1510
1521
  if (command === 'help' || flags.help || flags.h) {
1511
1522
  // Check for subcommand-specific help: nansen <command> <subcommand> --help
1512
1523
  if (flags.help || flags.h) {
1524
+ // Handle 'research <category> <sub> --help' (3-level)
1525
+ if (command === 'research' && subcommand) {
1526
+ const category = RESEARCH_CATEGORY_ALIASES[subcommand] || subcommand;
1527
+ const deepSub = subArgs[1];
1528
+ if (deepSub) {
1529
+ const subHelp = generateSubcommandHelp(category, deepSub);
1530
+ if (subHelp) {
1531
+ output(subHelp);
1532
+ notify();
1533
+ return { type: 'subcommand-help', command: category, subcommand: deepSub };
1534
+ }
1535
+ }
1536
+ // List category subcommands: 'nansen research smart-money --help'
1537
+ const researchCat = SCHEMA.commands.research.subcommands[category];
1538
+ if (researchCat) {
1539
+ const catSchema = researchCat;
1540
+ const lines = [`\nresearch ${category} - ${catSchema.description}\n`];
1541
+ if (catSchema.subcommands) {
1542
+ lines.push('SUBCOMMANDS:');
1543
+ for (const [sub, subSchema] of Object.entries(catSchema.subcommands)) {
1544
+ lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
1545
+ }
1546
+ lines.push(`\nFor detailed help: nansen research ${category} <subcommand> --help`);
1547
+ }
1548
+ output(lines.join('\n'));
1549
+ notify();
1550
+ return { type: 'command-help', command: `research ${category}` };
1551
+ }
1552
+ }
1553
+ // Handle 'trade <sub> --help'
1554
+ if (command === 'trade' && subcommand) {
1555
+ const tradeSchema = SCHEMA.commands.trade?.subcommands?.[subcommand];
1556
+ if (tradeSchema) {
1557
+ output(`\ntrade ${subcommand} - ${tradeSchema.description}\n`);
1558
+ notify();
1559
+ return { type: 'subcommand-help', command: 'trade', subcommand };
1560
+ }
1561
+ }
1513
1562
  // First try subcommand help
1514
1563
  if (command && subcommand) {
1515
1564
  const subHelp = generateSubcommandHelp(command, subcommand);
@@ -1520,14 +1569,17 @@ export async function runCLI(rawArgs, deps = {}) {
1520
1569
  }
1521
1570
  }
1522
1571
  // Then try command-level help (list subcommands)
1523
- if (command && SCHEMA.commands[command]) {
1524
- const cmdSchema = SCHEMA.commands[command];
1572
+ const cmdSchemaLookup = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
1573
+ if (command && cmdSchemaLookup) {
1574
+ const cmdSchema = cmdSchemaLookup;
1525
1575
  const lines = [`\n${command} - ${cmdSchema.description}\n`];
1526
- lines.push('SUBCOMMANDS:');
1527
- for (const [sub, subSchema] of Object.entries(cmdSchema.subcommands || {})) {
1528
- lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
1576
+ if (cmdSchema.subcommands) {
1577
+ lines.push('SUBCOMMANDS:');
1578
+ for (const [sub, subSchema] of Object.entries(cmdSchema.subcommands)) {
1579
+ lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
1580
+ }
1581
+ lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
1529
1582
  }
1530
- lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
1531
1583
  output(lines.join('\n'));
1532
1584
  notify();
1533
1585
  return { type: 'command-help', command };
@@ -1573,12 +1625,13 @@ export async function runCLI(rawArgs, deps = {}) {
1573
1625
  // Configure retry options
1574
1626
  const retryOptions = flags['no-retry']
1575
1627
  ? { maxRetries: 0 }
1576
- : { maxRetries: options.retries !== undefined ? options.retries : 3 };
1628
+ : { maxRetries: options.retries !== undefined ? (Number.isNaN(parseInt(options.retries, 10)) ? 3 : parseInt(options.retries, 10)) : 3 };
1577
1629
 
1578
1630
  // Configure cache options
1631
+ const cacheTtl = options['cache-ttl'] !== undefined ? parseInt(options['cache-ttl'], 10) : 300;
1579
1632
  const cacheOptions = {
1580
1633
  enabled: flags['cache'] && !flags['no-cache'],
1581
- ttl: options['cache-ttl'] !== undefined ? options['cache-ttl'] : 300
1634
+ ttl: Number.isNaN(cacheTtl) ? 300 : cacheTtl
1582
1635
  };
1583
1636
 
1584
1637
  const defaultHeaders = {};