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