nansen-cli 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +118 -164
- package/CLAUDE.md +16 -19
- package/README.md +163 -112
- package/SKILL.md +170 -76
- package/package.json +3 -1
- package/scripts/check-changeset.js +28 -0
- package/src/api.js +92 -70
- package/src/chain-ids.js +19 -0
- package/src/cli.js +406 -353
- package/src/ens.js +163 -0
- package/src/trading.js +324 -25
- package/src/transfer.js +133 -2
- package/src/update-check.js +35 -0
- package/src/wallet.js +11 -9
- package/src/walletconnect-exec.js +22 -0
- package/src/walletconnect-trading.js +91 -0
- package/src/walletconnect-x402.js +215 -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,140 @@ 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
|
-
|
|
144
|
+
chain: { type: 'string', default: 'ethereum', description: 'Blockchain' },
|
|
145
|
+
from: { type: 'string', required: true, description: 'Token to sell (address or symbol)' },
|
|
146
|
+
to: { type: 'string', required: true, description: 'Token to buy (address or symbol)' },
|
|
147
|
+
amount: { type: 'string', required: true, description: 'Amount to swap' },
|
|
148
|
+
wallet: { type: 'string', description: 'Wallet name, or "walletconnect"/"wc" for WalletConnect (EVM only)' }
|
|
149
|
+
}
|
|
279
150
|
},
|
|
280
|
-
'
|
|
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',
|
|
151
|
+
'execute': {
|
|
152
|
+
description: 'Sign and broadcast a quoted trade',
|
|
307
153
|
options: {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
returns: ['rank', 'address', 'address_label', 'points', 'tier']
|
|
154
|
+
chain: { type: 'string', default: 'ethereum', description: 'Blockchain' },
|
|
155
|
+
wallet: { type: 'string', description: 'Wallet name, or "walletconnect"/"wc" for WalletConnect (EVM only)' }
|
|
156
|
+
}
|
|
312
157
|
}
|
|
313
158
|
}
|
|
314
159
|
}
|
|
@@ -376,6 +221,19 @@ export function parseFields(fieldsOption) {
|
|
|
376
221
|
}
|
|
377
222
|
|
|
378
223
|
// Parse command line arguments
|
|
224
|
+
/**
|
|
225
|
+
* Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
226
|
+
*/
|
|
227
|
+
function compareSemver(a, b) {
|
|
228
|
+
const parse = v => v.replace(/^v/, '').split('.').map(Number);
|
|
229
|
+
const [aM, am, ap] = parse(a);
|
|
230
|
+
const [bM, bm, bp] = parse(b);
|
|
231
|
+
if (aM !== bM) return aM > bM ? 1 : -1;
|
|
232
|
+
if (am !== bm) return am > bm ? 1 : -1;
|
|
233
|
+
if (ap !== bp) return ap > bp ? 1 : -1;
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
379
237
|
export function parseArgs(args) {
|
|
380
238
|
const result = { _: [], flags: {}, options: {} };
|
|
381
239
|
|
|
@@ -389,9 +247,12 @@ export function parseArgs(args) {
|
|
|
389
247
|
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich') {
|
|
390
248
|
result.flags[key] = true;
|
|
391
249
|
} else if (next && !next.startsWith('-')) {
|
|
392
|
-
// Try to parse as JSON first
|
|
250
|
+
// Try to parse as JSON first (for objects/arrays/booleans),
|
|
251
|
+
// but keep numeric strings as strings to avoid precision loss
|
|
252
|
+
// and scientific notation for large integers (e.g. 1e+21).
|
|
393
253
|
try {
|
|
394
|
-
|
|
254
|
+
const parsed = JSON.parse(next);
|
|
255
|
+
result.options[key] = typeof parsed === 'number' ? next : parsed;
|
|
395
256
|
} catch {
|
|
396
257
|
result.options[key] = next;
|
|
397
258
|
}
|
|
@@ -672,8 +533,24 @@ export async function batchProfile(api, params = {}) {
|
|
|
672
533
|
const { addresses = [], chain = 'ethereum', include = ['labels', 'balance'], delayMs = 1000 } = params;
|
|
673
534
|
const results = [];
|
|
674
535
|
for (let i = 0; i < addresses.length; i++) {
|
|
675
|
-
|
|
536
|
+
let address = addresses[i].trim();
|
|
676
537
|
const entry = { address, chain };
|
|
538
|
+
|
|
539
|
+
// Resolve ENS names
|
|
540
|
+
if (isEnsName(address)) {
|
|
541
|
+
try {
|
|
542
|
+
const resolved = await resolveAddress(address, chain);
|
|
543
|
+
entry.ensName = resolved.ensName;
|
|
544
|
+
address = resolved.address;
|
|
545
|
+
entry.address = address;
|
|
546
|
+
} catch (err) {
|
|
547
|
+
entry.error = err.message;
|
|
548
|
+
results.push(entry);
|
|
549
|
+
if (i < addresses.length - 1) await sleep(delayMs);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
677
554
|
const validation = validateAddress(address, chain);
|
|
678
555
|
if (!validation.valid) {
|
|
679
556
|
entry.error = validation.error;
|
|
@@ -701,10 +578,21 @@ export async function batchProfile(api, params = {}) {
|
|
|
701
578
|
}
|
|
702
579
|
|
|
703
580
|
export async function traceCounterparties(api, params = {}) {
|
|
704
|
-
|
|
581
|
+
let { address, chain = 'ethereum', depth = 2, width = 10, days = 30, delayMs = 1000 } = params;
|
|
705
582
|
if (!address) {
|
|
706
583
|
throw new NansenError('address is required for trace', ErrorCode.MISSING_PARAM);
|
|
707
584
|
}
|
|
585
|
+
|
|
586
|
+
// Resolve ENS names
|
|
587
|
+
if (isEnsName(address)) {
|
|
588
|
+
try {
|
|
589
|
+
const resolved = await resolveAddress(address, chain);
|
|
590
|
+
address = resolved.address;
|
|
591
|
+
} catch (err) {
|
|
592
|
+
throw new NansenError(err.message, ErrorCode.INVALID_ADDRESS);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
708
596
|
const validation = validateAddress(address, chain);
|
|
709
597
|
if (!validation.valid) {
|
|
710
598
|
throw new NansenError(validation.error, ErrorCode.INVALID_ADDRESS);
|
|
@@ -838,23 +726,13 @@ USAGE:
|
|
|
838
726
|
nansen <command> [subcommand] [options]
|
|
839
727
|
|
|
840
728
|
COMMANDS:
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
schema Output JSON schema for all commands (for agent introspection)
|
|
729
|
+
research Research & analytics (smart-money, profiler, token, search, perp, portfolio, points)
|
|
730
|
+
trade DEX trading (quote, execute)
|
|
844
731
|
wallet Local wallet management (create, list, show, export, default, delete)
|
|
845
|
-
|
|
846
|
-
|
|
732
|
+
login/logout API key management
|
|
733
|
+
schema Output JSON schema for all commands (for agent introspection)
|
|
847
734
|
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)
|
|
735
|
+
changelog Show what's new (use --since <version> to filter)
|
|
858
736
|
help Show this help message
|
|
859
737
|
|
|
860
738
|
GLOBAL OPTIONS:
|
|
@@ -880,22 +758,19 @@ GLOBAL OPTIONS:
|
|
|
880
758
|
|
|
881
759
|
EXAMPLES:
|
|
882
760
|
# Get Smart Money netflow on Solana
|
|
883
|
-
nansen smart-money netflow --chain solana
|
|
761
|
+
nansen research smart-money netflow --chain solana
|
|
884
762
|
|
|
885
763
|
# Get top tokens by Smart Money activity
|
|
886
|
-
nansen token screener --chain solana --timeframe 24h --pretty
|
|
764
|
+
nansen research token screener --chain solana --timeframe 24h --pretty
|
|
887
765
|
|
|
888
766
|
# Get wallet balance
|
|
889
|
-
nansen profiler balance --address 0x123... --chain ethereum
|
|
890
|
-
|
|
891
|
-
# Get wallet labels
|
|
892
|
-
nansen profiler labels --address 0x123... --chain ethereum
|
|
767
|
+
nansen research profiler balance --address 0x123... --chain ethereum
|
|
893
768
|
|
|
894
|
-
# Search for
|
|
895
|
-
nansen
|
|
769
|
+
# Search for tokens/entities
|
|
770
|
+
nansen research search "Vitalik"
|
|
896
771
|
|
|
897
|
-
# Get
|
|
898
|
-
nansen
|
|
772
|
+
# Get a DEX swap quote
|
|
773
|
+
nansen trade quote --chain ethereum --from ETH --to USDC --amount 1
|
|
899
774
|
|
|
900
775
|
SMART MONEY LABELS:
|
|
901
776
|
Fund, Smart Trader, 30D Smart Trader, 90D Smart Trader,
|
|
@@ -964,52 +839,53 @@ export function buildCommands(deps = {}) {
|
|
|
964
839
|
api = null,
|
|
965
840
|
promptFn = prompt,
|
|
966
841
|
log = console.log,
|
|
842
|
+
errorOutput = console.error,
|
|
967
843
|
NansenAPIClass = NansenAPI,
|
|
968
844
|
saveConfigFn = saveConfig,
|
|
969
845
|
deleteConfigFn = deleteConfig,
|
|
970
846
|
getConfigFileFn = getConfigFile,
|
|
971
|
-
exit = process.exit
|
|
847
|
+
exit = process.exit,
|
|
848
|
+
isTTY = process.stdin.isTTY
|
|
972
849
|
} = deps;
|
|
973
850
|
|
|
974
|
-
|
|
851
|
+
const cmds = {
|
|
975
852
|
'login': async (args, apiInstance, flags, options) => {
|
|
976
853
|
// Support non-interactive: nansen login --api-key <key>
|
|
977
854
|
let apiKey = options['api-key'] || options.apiKey;
|
|
978
855
|
|
|
979
856
|
if (!apiKey) {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
857
|
+
if (!isTTY) {
|
|
858
|
+
// Non-interactive mode: check env var fallback
|
|
859
|
+
apiKey = process.env.NANSEN_API_KEY;
|
|
860
|
+
if (!apiKey) {
|
|
861
|
+
log('❌ No API key provided. Use: nansen login --api-key <key>\n Or set NANSEN_API_KEY environment variable.');
|
|
862
|
+
exit(1);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
} else {
|
|
866
|
+
log('Nansen CLI Login\n');
|
|
867
|
+
log('Get your API key at: https://app.nansen.ai/api\n');
|
|
868
|
+
log('Tip: For non-interactive use: nansen login --api-key <key>\n');
|
|
869
|
+
|
|
870
|
+
apiKey = await promptFn('Enter your API key: ', true);
|
|
871
|
+
}
|
|
985
872
|
}
|
|
986
|
-
|
|
873
|
+
|
|
987
874
|
if (!apiKey || apiKey.trim().length === 0) {
|
|
988
875
|
log('\n❌ No API key provided');
|
|
989
876
|
exit(1);
|
|
990
877
|
return;
|
|
991
878
|
}
|
|
992
|
-
|
|
993
|
-
//
|
|
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
|
-
}
|
|
879
|
+
|
|
880
|
+
// Save the config without validation
|
|
881
|
+
saveConfigFn({
|
|
882
|
+
apiKey: apiKey.trim(),
|
|
883
|
+
baseUrl: 'https://api.nansen.ai'
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
log(`✓ Saved to ${getConfigFileFn()}\n`);
|
|
887
|
+
log('You can now use the Nansen CLI. Try:');
|
|
888
|
+
log(' nansen token screener --chain solana --pretty');
|
|
1013
889
|
},
|
|
1014
890
|
|
|
1015
891
|
'logout': async (args, apiInstance, flags, options) => {
|
|
@@ -1025,21 +901,56 @@ export function buildCommands(deps = {}) {
|
|
|
1025
901
|
log(HELP);
|
|
1026
902
|
},
|
|
1027
903
|
|
|
904
|
+
'changelog': async (args, apiInstance, flags, options) => {
|
|
905
|
+
const changelogPath = new URL('../CHANGELOG.md', import.meta.url).pathname;
|
|
906
|
+
let content;
|
|
907
|
+
try {
|
|
908
|
+
content = fs.readFileSync(changelogPath, 'utf8');
|
|
909
|
+
} catch {
|
|
910
|
+
log('CHANGELOG.md not found. Visit https://github.com/nansen-ai/nansen-cli/blob/main/CHANGELOG.md');
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const since = options.since;
|
|
914
|
+
if (since) {
|
|
915
|
+
// Show only entries from the given version onwards
|
|
916
|
+
const lines = content.split('\n');
|
|
917
|
+
const filtered = [];
|
|
918
|
+
let include = false;
|
|
919
|
+
for (const line of lines) {
|
|
920
|
+
// Match ## [x.y.z] headers
|
|
921
|
+
const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
|
|
922
|
+
if (match) {
|
|
923
|
+
const ver = match[1];
|
|
924
|
+
// Compare: include versions >= since, stop at versions < since
|
|
925
|
+
if (compareSemver(ver, since) >= 0) {
|
|
926
|
+
include = true;
|
|
927
|
+
} else {
|
|
928
|
+
include = false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (include) filtered.push(line);
|
|
932
|
+
}
|
|
933
|
+
log(filtered.join('\n') || `No changelog entries found for versions >= ${since}`);
|
|
934
|
+
} else {
|
|
935
|
+
log(content);
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
|
|
1028
939
|
'schema': async (args, apiInstance, flags, options) => {
|
|
1029
940
|
// Return schema for agent introspection
|
|
1030
941
|
const subcommand = args[0];
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
942
|
+
const schemaEntry = subcommand && (SCHEMA.commands[subcommand] || SCHEMA.commands.research.subcommands[subcommand]);
|
|
943
|
+
|
|
944
|
+
if (schemaEntry) {
|
|
1034
945
|
return {
|
|
1035
946
|
command: subcommand,
|
|
1036
|
-
...
|
|
947
|
+
...schemaEntry,
|
|
1037
948
|
globalOptions: SCHEMA.globalOptions,
|
|
1038
949
|
chains: SCHEMA.chains,
|
|
1039
950
|
smartMoneyLabels: SCHEMA.smartMoneyLabels
|
|
1040
951
|
};
|
|
1041
952
|
}
|
|
1042
|
-
|
|
953
|
+
|
|
1043
954
|
// Return full schema
|
|
1044
955
|
return SCHEMA;
|
|
1045
956
|
},
|
|
@@ -1113,9 +1024,21 @@ export function buildCommands(deps = {}) {
|
|
|
1113
1024
|
|
|
1114
1025
|
'profiler': async (args, apiInstance, flags, options) => {
|
|
1115
1026
|
const subcommand = args[0] || 'help';
|
|
1116
|
-
|
|
1027
|
+
let address = options.address;
|
|
1117
1028
|
const entityName = options.entity || options['entity-name'];
|
|
1118
1029
|
const chain = options.chain || 'ethereum';
|
|
1030
|
+
|
|
1031
|
+
// Resolve ENS names (e.g. vitalik.eth → 0x...)
|
|
1032
|
+
let ensName;
|
|
1033
|
+
if (address && isEnsName(address)) {
|
|
1034
|
+
try {
|
|
1035
|
+
const resolved = await resolveAddress(address, chain);
|
|
1036
|
+
address = resolved.address;
|
|
1037
|
+
ensName = resolved.ensName;
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
throw new NansenError(err.message, ErrorCode.INVALID_ADDRESS);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1119
1042
|
const filters = options.filters || {};
|
|
1120
1043
|
const orderBy = parseSort(options.sort, options['order-by']);
|
|
1121
1044
|
const pagination = options.limit ? { page: 1, recordsPerPage: options.limit } : undefined;
|
|
@@ -1187,7 +1110,12 @@ export function buildCommands(deps = {}) {
|
|
|
1187
1110
|
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
1188
1111
|
}
|
|
1189
1112
|
|
|
1190
|
-
|
|
1113
|
+
const result = await handlers[subcommand]();
|
|
1114
|
+
|
|
1115
|
+
// Attach ENS metadata so the caller knows the name was resolved
|
|
1116
|
+
return ensName && result && typeof result === 'object'
|
|
1117
|
+
? { ...result, _ens: { name: ensName, resolvedAddress: address } }
|
|
1118
|
+
: result;
|
|
1191
1119
|
},
|
|
1192
1120
|
|
|
1193
1121
|
'token': async (args, apiInstance, flags, options) => {
|
|
@@ -1211,6 +1139,7 @@ export function buildCommands(deps = {}) {
|
|
|
1211
1139
|
|
|
1212
1140
|
const handlers = {
|
|
1213
1141
|
'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
|
|
1142
|
+
'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d', pagination }),
|
|
1214
1143
|
'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
|
|
1215
1144
|
'screener': async () => {
|
|
1216
1145
|
const search = options.search;
|
|
@@ -1259,7 +1188,7 @@ export function buildCommands(deps = {}) {
|
|
|
1259
1188
|
'perp-positions': () => apiInstance.tokenPerpPositions({ tokenSymbol, filters, orderBy, pagination }),
|
|
1260
1189
|
'perp-pnl-leaderboard': () => apiInstance.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days }),
|
|
1261
1190
|
'help': () => ({
|
|
1262
|
-
commands: ['info', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
1191
|
+
commands: ['info', 'ohlcv', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
1263
1192
|
description: 'Token God Mode endpoints',
|
|
1264
1193
|
example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
|
|
1265
1194
|
})
|
|
@@ -1354,12 +1283,73 @@ export function buildCommands(deps = {}) {
|
|
|
1354
1283
|
return handlers[subcommand]();
|
|
1355
1284
|
}
|
|
1356
1285
|
};
|
|
1286
|
+
|
|
1287
|
+
// 'research' delegates to the category handlers defined above
|
|
1288
|
+
const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points']);
|
|
1289
|
+
|
|
1290
|
+
cmds['research'] = async (args, apiInstance, flags, options) => {
|
|
1291
|
+
const rawCategory = args[0];
|
|
1292
|
+
if (!rawCategory || rawCategory === 'help') {
|
|
1293
|
+
return {
|
|
1294
|
+
categories: [...RESEARCH_CATEGORIES],
|
|
1295
|
+
aliases: RESEARCH_CATEGORY_ALIASES,
|
|
1296
|
+
description: 'Research and analytics commands',
|
|
1297
|
+
example: 'nansen research smart-money netflow --chain solana'
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
const category = RESEARCH_CATEGORY_ALIASES[rawCategory] || rawCategory;
|
|
1301
|
+
if (!RESEARCH_CATEGORIES.has(category)) {
|
|
1302
|
+
return { error: `Unknown research category: ${rawCategory}`, available: [...RESEARCH_CATEGORIES] };
|
|
1303
|
+
}
|
|
1304
|
+
return cmds[category](args.slice(1), apiInstance, flags, options);
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// 'trade' delegates to quote/execute from buildTradingCommands
|
|
1308
|
+
const tradingCmds = buildTradingCommands(deps);
|
|
1309
|
+
cmds['trade'] = async (args, apiInstance, flags, options) => {
|
|
1310
|
+
const sub = args[0];
|
|
1311
|
+
if (!sub || sub === 'help') {
|
|
1312
|
+
log(`nansen trade — DEX trading commands
|
|
1313
|
+
|
|
1314
|
+
SUBCOMMANDS:
|
|
1315
|
+
quote Get a swap quote (price, route, fees)
|
|
1316
|
+
execute Sign and broadcast a quoted swap
|
|
1317
|
+
|
|
1318
|
+
USAGE:
|
|
1319
|
+
nansen trade quote --chain <chain> --from <token> --to <token> --amount <units>
|
|
1320
|
+
nansen trade execute --quote <quoteId>
|
|
1321
|
+
|
|
1322
|
+
EXAMPLES:
|
|
1323
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
1324
|
+
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
1325
|
+
nansen trade execute --quote 1708900000000-abc123
|
|
1326
|
+
|
|
1327
|
+
SYMBOLS:
|
|
1328
|
+
Common tokens resolve automatically: SOL, ETH, BNB, USDC, USDT, WETH, WBNB
|
|
1329
|
+
Raw addresses are also accepted.`);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (!tradingCmds[sub]) {
|
|
1333
|
+
log(`Unknown trade subcommand: ${sub}`);
|
|
1334
|
+
log(`Available: quote, execute`);
|
|
1335
|
+
log(`Run 'nansen trade help' for usage.`);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
return cmds;
|
|
1357
1342
|
}
|
|
1358
1343
|
|
|
1344
|
+
// Categories that moved under 'research'
|
|
1345
|
+
export const DEPRECATED_TO_RESEARCH = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points']);
|
|
1346
|
+
// Subcommands that moved under 'trade'
|
|
1347
|
+
export const DEPRECATED_TO_TRADE = new Set(['quote', 'execute']);
|
|
1348
|
+
|
|
1359
1349
|
// Commands that don't require API authentication
|
|
1360
|
-
export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'quote', 'execute'];
|
|
1350
|
+
export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'trade', 'quote', 'execute', 'changelog'];
|
|
1361
1351
|
|
|
1362
|
-
// Command aliases
|
|
1352
|
+
// Command aliases: top-level shortcuts that resolve before routing
|
|
1363
1353
|
export const COMMAND_ALIASES = {
|
|
1364
1354
|
'tgm': 'token', // Token God Mode
|
|
1365
1355
|
'sm': 'smart-money', // Smart Money
|
|
@@ -1367,11 +1357,20 @@ export const COMMAND_ALIASES = {
|
|
|
1367
1357
|
'port': 'portfolio' // Portfolio
|
|
1368
1358
|
};
|
|
1369
1359
|
|
|
1360
|
+
// Aliases used inside the 'research' namespace
|
|
1361
|
+
export const RESEARCH_CATEGORY_ALIASES = {
|
|
1362
|
+
'tgm': 'token',
|
|
1363
|
+
'sm': 'smart-money',
|
|
1364
|
+
'prof': 'profiler',
|
|
1365
|
+
'port': 'portfolio'
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1370
1368
|
// Generate help text for a specific subcommand using SCHEMA
|
|
1371
1369
|
export function generateSubcommandHelp(command, subcommand) {
|
|
1372
|
-
|
|
1370
|
+
// Look up in top-level commands, then fall back to research subcommands
|
|
1371
|
+
const cmdSchema = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
|
|
1373
1372
|
if (!cmdSchema) return null;
|
|
1374
|
-
|
|
1373
|
+
|
|
1375
1374
|
const subSchema = cmdSchema.subcommands?.[subcommand];
|
|
1376
1375
|
if (!subSchema) return null;
|
|
1377
1376
|
|
|
@@ -1490,6 +1489,14 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1490
1489
|
const command = COMMAND_ALIASES[rawCommand] || rawCommand;
|
|
1491
1490
|
const subArgs = positional.slice(1);
|
|
1492
1491
|
const subcommand = subArgs[0];
|
|
1492
|
+
|
|
1493
|
+
// Deprecation warnings for commands that moved under 'research' or 'trade'
|
|
1494
|
+
if (DEPRECATED_TO_RESEARCH.has(command)) {
|
|
1495
|
+
errorOutput(`Warning: "nansen ${command}" is deprecated. Use "nansen research ${command}" instead.`);
|
|
1496
|
+
} else if (DEPRECATED_TO_TRADE.has(command)) {
|
|
1497
|
+
errorOutput(`Warning: "nansen ${command}" is deprecated. Use "nansen trade ${command}" instead.`);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1493
1500
|
const pretty = flags.pretty || flags.p;
|
|
1494
1501
|
const table = flags.table || flags.t;
|
|
1495
1502
|
const stream = flags.stream || flags.s;
|
|
@@ -1497,8 +1504,12 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1497
1504
|
|
|
1498
1505
|
// Update check (read cached result + schedule background refresh)
|
|
1499
1506
|
const updateNotification = getUpdateNotification(VERSION);
|
|
1507
|
+
const upgradeNotice = getUpgradeNotice(VERSION);
|
|
1500
1508
|
scheduleUpdateCheck();
|
|
1501
|
-
const notify = () => {
|
|
1509
|
+
const notify = () => {
|
|
1510
|
+
if (upgradeNotice) errorOutput(upgradeNotice);
|
|
1511
|
+
if (updateNotification) errorOutput(updateNotification);
|
|
1512
|
+
};
|
|
1502
1513
|
|
|
1503
1514
|
const commands = { ...buildCommands(deps), ...buildWalletCommands(deps), ...buildTradingCommands(deps), ...commandOverrides };
|
|
1504
1515
|
|
|
@@ -1510,6 +1521,44 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1510
1521
|
if (command === 'help' || flags.help || flags.h) {
|
|
1511
1522
|
// Check for subcommand-specific help: nansen <command> <subcommand> --help
|
|
1512
1523
|
if (flags.help || flags.h) {
|
|
1524
|
+
// Handle 'research <category> <sub> --help' (3-level)
|
|
1525
|
+
if (command === 'research' && subcommand) {
|
|
1526
|
+
const category = RESEARCH_CATEGORY_ALIASES[subcommand] || subcommand;
|
|
1527
|
+
const deepSub = subArgs[1];
|
|
1528
|
+
if (deepSub) {
|
|
1529
|
+
const subHelp = generateSubcommandHelp(category, deepSub);
|
|
1530
|
+
if (subHelp) {
|
|
1531
|
+
output(subHelp);
|
|
1532
|
+
notify();
|
|
1533
|
+
return { type: 'subcommand-help', command: category, subcommand: deepSub };
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
// List category subcommands: 'nansen research smart-money --help'
|
|
1537
|
+
const researchCat = SCHEMA.commands.research.subcommands[category];
|
|
1538
|
+
if (researchCat) {
|
|
1539
|
+
const catSchema = researchCat;
|
|
1540
|
+
const lines = [`\nresearch ${category} - ${catSchema.description}\n`];
|
|
1541
|
+
if (catSchema.subcommands) {
|
|
1542
|
+
lines.push('SUBCOMMANDS:');
|
|
1543
|
+
for (const [sub, subSchema] of Object.entries(catSchema.subcommands)) {
|
|
1544
|
+
lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
|
|
1545
|
+
}
|
|
1546
|
+
lines.push(`\nFor detailed help: nansen research ${category} <subcommand> --help`);
|
|
1547
|
+
}
|
|
1548
|
+
output(lines.join('\n'));
|
|
1549
|
+
notify();
|
|
1550
|
+
return { type: 'command-help', command: `research ${category}` };
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
// Handle 'trade <sub> --help'
|
|
1554
|
+
if (command === 'trade' && subcommand) {
|
|
1555
|
+
const tradeSchema = SCHEMA.commands.trade?.subcommands?.[subcommand];
|
|
1556
|
+
if (tradeSchema) {
|
|
1557
|
+
output(`\ntrade ${subcommand} - ${tradeSchema.description}\n`);
|
|
1558
|
+
notify();
|
|
1559
|
+
return { type: 'subcommand-help', command: 'trade', subcommand };
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1513
1562
|
// First try subcommand help
|
|
1514
1563
|
if (command && subcommand) {
|
|
1515
1564
|
const subHelp = generateSubcommandHelp(command, subcommand);
|
|
@@ -1520,14 +1569,17 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1520
1569
|
}
|
|
1521
1570
|
}
|
|
1522
1571
|
// Then try command-level help (list subcommands)
|
|
1523
|
-
|
|
1524
|
-
|
|
1572
|
+
const cmdSchemaLookup = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
|
|
1573
|
+
if (command && cmdSchemaLookup) {
|
|
1574
|
+
const cmdSchema = cmdSchemaLookup;
|
|
1525
1575
|
const lines = [`\n${command} - ${cmdSchema.description}\n`];
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1576
|
+
if (cmdSchema.subcommands) {
|
|
1577
|
+
lines.push('SUBCOMMANDS:');
|
|
1578
|
+
for (const [sub, subSchema] of Object.entries(cmdSchema.subcommands)) {
|
|
1579
|
+
lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
|
|
1580
|
+
}
|
|
1581
|
+
lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
|
|
1529
1582
|
}
|
|
1530
|
-
lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
|
|
1531
1583
|
output(lines.join('\n'));
|
|
1532
1584
|
notify();
|
|
1533
1585
|
return { type: 'command-help', command };
|
|
@@ -1573,12 +1625,13 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1573
1625
|
// Configure retry options
|
|
1574
1626
|
const retryOptions = flags['no-retry']
|
|
1575
1627
|
? { maxRetries: 0 }
|
|
1576
|
-
: { maxRetries: options.retries !== undefined ? options.retries : 3 };
|
|
1628
|
+
: { maxRetries: options.retries !== undefined ? (Number.isNaN(parseInt(options.retries, 10)) ? 3 : parseInt(options.retries, 10)) : 3 };
|
|
1577
1629
|
|
|
1578
1630
|
// Configure cache options
|
|
1631
|
+
const cacheTtl = options['cache-ttl'] !== undefined ? parseInt(options['cache-ttl'], 10) : 300;
|
|
1579
1632
|
const cacheOptions = {
|
|
1580
1633
|
enabled: flags['cache'] && !flags['no-cache'],
|
|
1581
|
-
ttl:
|
|
1634
|
+
ttl: Number.isNaN(cacheTtl) ? 300 : cacheTtl
|
|
1582
1635
|
};
|
|
1583
1636
|
|
|
1584
1637
|
const defaultHeaders = {};
|