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/AGENTS.md +118 -164
- package/CLAUDE.md +16 -19
- package/README.md +163 -112
- package/package.json +3 -1
- package/scripts/check-changeset.js +28 -0
- package/src/api.js +41 -59
- package/src/cli.js +387 -353
- package/src/ens.js +163 -0
- package/src/update-check.js +35 -0
- package/vitest.e2e.config.js +10 -0
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
|
-
'
|
|
23
|
-
description: '
|
|
23
|
+
'research': {
|
|
24
|
+
description: 'Research and analytics commands',
|
|
24
25
|
subcommands: {
|
|
25
|
-
'
|
|
26
|
-
description: '
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
'
|
|
89
|
-
description: '
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
'
|
|
94
|
-
description: '
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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: ['
|
|
115
|
+
returns: ['tokens[name, symbol, chain, address, price, volume_24h, market_cap, rank]', 'entities[name, tags, rank]', 'total_results']
|
|
177
116
|
},
|
|
178
|
-
'
|
|
179
|
-
description: '
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
'
|
|
246
|
-
description: '
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
'
|
|
251
|
-
description: '
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
'
|
|
268
|
-
description: '
|
|
138
|
+
'trade': {
|
|
139
|
+
description: 'DEX trading commands',
|
|
269
140
|
subcommands: {
|
|
270
|
-
'
|
|
271
|
-
description: '
|
|
141
|
+
'quote': {
|
|
142
|
+
description: 'Get a DEX swap quote (chain, tokens, amount)',
|
|
272
143
|
options: {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
'
|
|
281
|
-
description: '
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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
|
-
|
|
846
|
-
|
|
731
|
+
login/logout API key management
|
|
732
|
+
schema Output JSON schema for all commands (for agent introspection)
|
|
847
733
|
cache Cache management (clear)
|
|
848
|
-
|
|
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
|
-
#
|
|
892
|
-
nansen
|
|
768
|
+
# Search for tokens/entities
|
|
769
|
+
nansen research search "Vitalik"
|
|
893
770
|
|
|
894
|
-
#
|
|
895
|
-
nansen
|
|
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
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
//
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
940
|
+
const schemaEntry = subcommand && (SCHEMA.commands[subcommand] || SCHEMA.commands.research.subcommands[subcommand]);
|
|
941
|
+
|
|
942
|
+
if (schemaEntry) {
|
|
1034
943
|
return {
|
|
1035
944
|
command: subcommand,
|
|
1036
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 = () => {
|
|
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
|
-
|
|
1524
|
-
|
|
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
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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:
|
|
1615
|
+
ttl: Number.isNaN(cacheTtl) ? 300 : cacheTtl
|
|
1582
1616
|
};
|
|
1583
1617
|
|
|
1584
1618
|
const defaultHeaders = {};
|