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