nansen-cli 1.7.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
@@ -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,139 @@ 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
+ }
279
149
  },
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',
150
+ 'execute': {
151
+ description: 'Sign and broadcast a quoted trade',
307
152
  options: {
308
- tier: { type: 'string', description: 'Filter by tier' },
309
- limit: { type: 'number' }
310
- },
311
- 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
+ }
312
156
  }
313
157
  }
314
158
  }
@@ -376,6 +220,19 @@ export function parseFields(fieldsOption) {
376
220
  }
377
221
 
378
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
+
379
236
  export function parseArgs(args) {
380
237
  const result = { _: [], flags: {}, options: {} };
381
238
 
@@ -389,9 +246,12 @@ export function parseArgs(args) {
389
246
  if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich') {
390
247
  result.flags[key] = true;
391
248
  } else if (next && !next.startsWith('-')) {
392
- // 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).
393
252
  try {
394
- result.options[key] = JSON.parse(next);
253
+ const parsed = JSON.parse(next);
254
+ result.options[key] = typeof parsed === 'number' ? next : parsed;
395
255
  } catch {
396
256
  result.options[key] = next;
397
257
  }
@@ -672,8 +532,24 @@ export async function batchProfile(api, params = {}) {
672
532
  const { addresses = [], chain = 'ethereum', include = ['labels', 'balance'], delayMs = 1000 } = params;
673
533
  const results = [];
674
534
  for (let i = 0; i < addresses.length; i++) {
675
- const address = addresses[i].trim();
535
+ let address = addresses[i].trim();
676
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
+
677
553
  const validation = validateAddress(address, chain);
678
554
  if (!validation.valid) {
679
555
  entry.error = validation.error;
@@ -701,10 +577,21 @@ export async function batchProfile(api, params = {}) {
701
577
  }
702
578
 
703
579
  export async function traceCounterparties(api, params = {}) {
704
- 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;
705
581
  if (!address) {
706
582
  throw new NansenError('address is required for trace', ErrorCode.MISSING_PARAM);
707
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
+
708
595
  const validation = validateAddress(address, chain);
709
596
  if (!validation.valid) {
710
597
  throw new NansenError(validation.error, ErrorCode.INVALID_ADDRESS);
@@ -838,23 +725,13 @@ USAGE:
838
725
  nansen <command> [subcommand] [options]
839
726
 
840
727
  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)
728
+ research Research & analytics (smart-money, profiler, token, search, perp, portfolio, points)
729
+ trade DEX trading (quote, execute)
844
730
  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
731
+ login/logout API key management
732
+ schema Output JSON schema for all commands (for agent introspection)
847
733
  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)
734
+ changelog Show what's new (use --since <version> to filter)
858
735
  help Show this help message
859
736
 
860
737
  GLOBAL OPTIONS:
@@ -880,22 +757,19 @@ GLOBAL OPTIONS:
880
757
 
881
758
  EXAMPLES:
882
759
  # Get Smart Money netflow on Solana
883
- nansen smart-money netflow --chain solana
760
+ nansen research smart-money netflow --chain solana
884
761
 
885
762
  # Get top tokens by Smart Money activity
886
- nansen token screener --chain solana --timeframe 24h --pretty
763
+ nansen research token screener --chain solana --timeframe 24h --pretty
887
764
 
888
765
  # Get wallet balance
889
- nansen profiler balance --address 0x123... --chain ethereum
766
+ nansen research profiler balance --address 0x123... --chain ethereum
890
767
 
891
- # Get wallet labels
892
- nansen profiler labels --address 0x123... --chain ethereum
768
+ # Search for tokens/entities
769
+ nansen research search "Vitalik"
893
770
 
894
- # Search for entity
895
- nansen profiler search --query "Vitalik"
896
-
897
- # Get token holders with filters
898
- 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
899
773
 
900
774
  SMART MONEY LABELS:
901
775
  Fund, Smart Trader, 30D Smart Trader, 90D Smart Trader,
@@ -968,48 +842,48 @@ export function buildCommands(deps = {}) {
968
842
  saveConfigFn = saveConfig,
969
843
  deleteConfigFn = deleteConfig,
970
844
  getConfigFileFn = getConfigFile,
971
- exit = process.exit
845
+ exit = process.exit,
846
+ isTTY = process.stdin.isTTY
972
847
  } = deps;
973
848
 
974
- return {
849
+ const cmds = {
975
850
  'login': async (args, apiInstance, flags, options) => {
976
851
  // Support non-interactive: nansen login --api-key <key>
977
852
  let apiKey = options['api-key'] || options.apiKey;
978
853
 
979
854
  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);
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
+ }
985
870
  }
986
-
871
+
987
872
  if (!apiKey || apiKey.trim().length === 0) {
988
873
  log('\n❌ No API key provided');
989
874
  exit(1);
990
875
  return;
991
876
  }
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
- }
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');
1013
887
  },
1014
888
 
1015
889
  'logout': async (args, apiInstance, flags, options) => {
@@ -1025,21 +899,56 @@ export function buildCommands(deps = {}) {
1025
899
  log(HELP);
1026
900
  },
1027
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
+
1028
937
  'schema': async (args, apiInstance, flags, options) => {
1029
938
  // Return schema for agent introspection
1030
939
  const subcommand = args[0];
1031
-
1032
- if (subcommand && SCHEMA.commands[subcommand]) {
1033
- // Return schema for specific command
940
+ const schemaEntry = subcommand && (SCHEMA.commands[subcommand] || SCHEMA.commands.research.subcommands[subcommand]);
941
+
942
+ if (schemaEntry) {
1034
943
  return {
1035
944
  command: subcommand,
1036
- ...SCHEMA.commands[subcommand],
945
+ ...schemaEntry,
1037
946
  globalOptions: SCHEMA.globalOptions,
1038
947
  chains: SCHEMA.chains,
1039
948
  smartMoneyLabels: SCHEMA.smartMoneyLabels
1040
949
  };
1041
950
  }
1042
-
951
+
1043
952
  // Return full schema
1044
953
  return SCHEMA;
1045
954
  },
@@ -1113,9 +1022,21 @@ export function buildCommands(deps = {}) {
1113
1022
 
1114
1023
  'profiler': async (args, apiInstance, flags, options) => {
1115
1024
  const subcommand = args[0] || 'help';
1116
- const address = options.address;
1025
+ let address = options.address;
1117
1026
  const entityName = options.entity || options['entity-name'];
1118
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
+ }
1119
1040
  const filters = options.filters || {};
1120
1041
  const orderBy = parseSort(options.sort, options['order-by']);
1121
1042
  const pagination = options.limit ? { page: 1, recordsPerPage: options.limit } : undefined;
@@ -1187,7 +1108,12 @@ export function buildCommands(deps = {}) {
1187
1108
  return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
1188
1109
  }
1189
1110
 
1190
- 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;
1191
1117
  },
1192
1118
 
1193
1119
  'token': async (args, apiInstance, flags, options) => {
@@ -1211,6 +1137,7 @@ export function buildCommands(deps = {}) {
1211
1137
 
1212
1138
  const handlers = {
1213
1139
  'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
1140
+ 'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d', pagination }),
1214
1141
  'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
1215
1142
  'screener': async () => {
1216
1143
  const search = options.search;
@@ -1259,7 +1186,7 @@ export function buildCommands(deps = {}) {
1259
1186
  'perp-positions': () => apiInstance.tokenPerpPositions({ tokenSymbol, filters, orderBy, pagination }),
1260
1187
  'perp-pnl-leaderboard': () => apiInstance.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days }),
1261
1188
  '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'],
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'],
1263
1190
  description: 'Token God Mode endpoints',
1264
1191
  example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
1265
1192
  })
@@ -1354,12 +1281,56 @@ export function buildCommands(deps = {}) {
1354
1281
  return handlers[subcommand]();
1355
1282
  }
1356
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;
1357
1323
  }
1358
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
+
1359
1330
  // Commands that don't require API authentication
1360
- export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'quote', 'execute'];
1331
+ export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'trade', 'quote', 'execute', 'changelog'];
1361
1332
 
1362
- // Command aliases for convenience
1333
+ // Command aliases: top-level shortcuts that resolve before routing
1363
1334
  export const COMMAND_ALIASES = {
1364
1335
  'tgm': 'token', // Token God Mode
1365
1336
  'sm': 'smart-money', // Smart Money
@@ -1367,11 +1338,20 @@ export const COMMAND_ALIASES = {
1367
1338
  'port': 'portfolio' // Portfolio
1368
1339
  };
1369
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
+
1370
1349
  // Generate help text for a specific subcommand using SCHEMA
1371
1350
  export function generateSubcommandHelp(command, subcommand) {
1372
- 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];
1373
1353
  if (!cmdSchema) return null;
1374
-
1354
+
1375
1355
  const subSchema = cmdSchema.subcommands?.[subcommand];
1376
1356
  if (!subSchema) return null;
1377
1357
 
@@ -1490,6 +1470,14 @@ export async function runCLI(rawArgs, deps = {}) {
1490
1470
  const command = COMMAND_ALIASES[rawCommand] || rawCommand;
1491
1471
  const subArgs = positional.slice(1);
1492
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
+
1493
1481
  const pretty = flags.pretty || flags.p;
1494
1482
  const table = flags.table || flags.t;
1495
1483
  const stream = flags.stream || flags.s;
@@ -1497,8 +1485,12 @@ export async function runCLI(rawArgs, deps = {}) {
1497
1485
 
1498
1486
  // Update check (read cached result + schedule background refresh)
1499
1487
  const updateNotification = getUpdateNotification(VERSION);
1488
+ const upgradeNotice = getUpgradeNotice(VERSION);
1500
1489
  scheduleUpdateCheck();
1501
- const notify = () => { if (updateNotification) errorOutput(updateNotification); };
1490
+ const notify = () => {
1491
+ if (upgradeNotice) errorOutput(upgradeNotice);
1492
+ if (updateNotification) errorOutput(updateNotification);
1493
+ };
1502
1494
 
1503
1495
  const commands = { ...buildCommands(deps), ...buildWalletCommands(deps), ...buildTradingCommands(deps), ...commandOverrides };
1504
1496
 
@@ -1510,6 +1502,44 @@ export async function runCLI(rawArgs, deps = {}) {
1510
1502
  if (command === 'help' || flags.help || flags.h) {
1511
1503
  // Check for subcommand-specific help: nansen <command> <subcommand> --help
1512
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
+ }
1513
1543
  // First try subcommand help
1514
1544
  if (command && subcommand) {
1515
1545
  const subHelp = generateSubcommandHelp(command, subcommand);
@@ -1520,14 +1550,17 @@ export async function runCLI(rawArgs, deps = {}) {
1520
1550
  }
1521
1551
  }
1522
1552
  // Then try command-level help (list subcommands)
1523
- if (command && SCHEMA.commands[command]) {
1524
- const cmdSchema = SCHEMA.commands[command];
1553
+ const cmdSchemaLookup = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
1554
+ if (command && cmdSchemaLookup) {
1555
+ const cmdSchema = cmdSchemaLookup;
1525
1556
  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 || ''}`);
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`);
1529
1563
  }
1530
- lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
1531
1564
  output(lines.join('\n'));
1532
1565
  notify();
1533
1566
  return { type: 'command-help', command };
@@ -1573,12 +1606,13 @@ export async function runCLI(rawArgs, deps = {}) {
1573
1606
  // Configure retry options
1574
1607
  const retryOptions = flags['no-retry']
1575
1608
  ? { maxRetries: 0 }
1576
- : { maxRetries: options.retries !== undefined ? options.retries : 3 };
1609
+ : { maxRetries: options.retries !== undefined ? (Number.isNaN(parseInt(options.retries, 10)) ? 3 : parseInt(options.retries, 10)) : 3 };
1577
1610
 
1578
1611
  // Configure cache options
1612
+ const cacheTtl = options['cache-ttl'] !== undefined ? parseInt(options['cache-ttl'], 10) : 300;
1579
1613
  const cacheOptions = {
1580
1614
  enabled: flags['cache'] && !flags['no-cache'],
1581
- ttl: options['cache-ttl'] !== undefined ? options['cache-ttl'] : 300
1615
+ ttl: Number.isNaN(cacheTtl) ? 300 : cacheTtl
1582
1616
  };
1583
1617
 
1584
1618
  const defaultHeaders = {};