nansen-cli 1.30.2 → 1.31.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.31.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#437](https://github.com/nansen-ai/nansen-cli/pull/437) [`5ec7bd3`](https://github.com/nansen-ai/nansen-cli/commit/5ec7bd33f173cd712f9f592599e32b2a0d28fe0f) Thanks [@gulshngill](https://github.com/gulshngill)! - Add `nansen research` command with 11 subcommands for historical/point-in-time analytics: dex-trades, pnl-leaderboard, token-flow-summary, token-quant-scores, top-holders, who-bought-sold, smart-money-balances, token-screener, wallet-balances, tx-lookup, wallet-transactions. Labels and metrics resolve at the requested date rather than current state — useful for backtesting and historical research.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#440](https://github.com/nansen-ai/nansen-cli/pull/440) [`701dad4`](https://github.com/nansen-ai/nansen-cli/commit/701dad49f54323cf2b455376b1af3b012e2e0b71) Thanks [@kome12](https://github.com/kome12)! - Fix `research historical-token-screener` schema to mark `--to-date` as required (matching CLI and API behavior)
12
+
13
+ - [#442](https://github.com/nansen-ai/nansen-cli/pull/442) [`6edbb68`](https://github.com/nansen-ai/nansen-cli/commit/6edbb686b52e00b8725470a3f4409ff15f2ccb95) Thanks [@kome12](https://github.com/kome12)! - `research historical-token-flow-summary` now errors immediately when `--page` or `--limit` are passed (the endpoint returns a single aggregated row and does not support pagination). `research historical-smart-money-balances` now errors when `--sort` or `--order-by` are passed (the endpoint does not support ordering). Previously both flags were silently dropped.
14
+
3
15
  ## 1.30.2
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.30.2",
3
+ "version": "1.31.0",
4
4
  "description": "AI-agent CLI for Nansen API analytics, DEX swaps, and cross-chain trading",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -66,6 +66,7 @@
66
66
  "@changesets/cli": "^2.29.4",
67
67
  "@eslint/js": "^10.0.1",
68
68
  "@vitest/coverage-v8": "^4.0.18",
69
+ "clawhub": "^0.9.0",
69
70
  "eslint": "^10.0.2",
70
71
  "globals": "^17.4.0",
71
72
  "vitest": "^4.0.18"
package/src/api.js CHANGED
@@ -1363,6 +1363,151 @@ export class NansenAPI {
1363
1363
  });
1364
1364
  }
1365
1365
 
1366
+ // ============= Research (Historical) Endpoints =============
1367
+ // Historical/point-in-time analytics: labels and metrics are resolved at the
1368
+ // requested date rather than current state. Useful for backtesting and historical
1369
+ // research. Some endpoints use a { from, to } date range; others use a single
1370
+ // as_of_date snapshot; the token-screener uses timeframe_days + optional to_date.
1371
+
1372
+ async researchDexTrades(params = {}) {
1373
+ const { tokenAddress, chain = 'solana', fromDate, toDate, filters = {}, orderBy, pagination } = params;
1374
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1375
+ return this.request('/api/v1beta1/tgm/historical-dex-trades', {
1376
+ token_address: tokenAddress,
1377
+ chain,
1378
+ date_range: { from: fromDate, to: toDate },
1379
+ filters,
1380
+ order_by: orderBy,
1381
+ pagination,
1382
+ });
1383
+ }
1384
+
1385
+ async researchPnlLeaderboard(params = {}) {
1386
+ const { tokenAddress, chain = 'solana', fromDate, toDate, filters = {}, orderBy, pagination } = params;
1387
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1388
+ return this.request('/api/v1beta1/tgm/historical-pnl-leaderboard', {
1389
+ token_address: tokenAddress,
1390
+ chain,
1391
+ date_range: { from: fromDate, to: toDate },
1392
+ filters,
1393
+ order_by: orderBy,
1394
+ pagination,
1395
+ });
1396
+ }
1397
+
1398
+ async researchTokenFlowSummary(params = {}) {
1399
+ // API does not support pagination on this endpoint.
1400
+ const { tokenAddress, chain = 'solana', fromDate, toDate, filters = {}, orderBy } = params;
1401
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1402
+ return this.request('/api/v1beta1/tgm/historical-token-flow-summary', {
1403
+ token_address: tokenAddress,
1404
+ chain,
1405
+ date_range: { from: fromDate, to: toDate },
1406
+ filters,
1407
+ order_by: orderBy,
1408
+ });
1409
+ }
1410
+
1411
+ async researchTokenQuantScores(params = {}) {
1412
+ const { tokenAddress, chain = 'solana', asOfDate, filters = {}, orderBy, pagination } = params;
1413
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1414
+ return this.request('/api/v1beta1/tgm/historical-token-quant-scores', {
1415
+ token_address: tokenAddress,
1416
+ chain,
1417
+ as_of_date: asOfDate,
1418
+ filters,
1419
+ order_by: orderBy,
1420
+ pagination,
1421
+ });
1422
+ }
1423
+
1424
+ async researchTopHolders(params = {}) {
1425
+ const { tokenAddress, chain = 'solana', asOfDate, filters = {}, orderBy, pagination } = params;
1426
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1427
+ return this.request('/api/v1beta1/tgm/historical-top-holders', {
1428
+ token_address: tokenAddress,
1429
+ chain,
1430
+ as_of_date: asOfDate,
1431
+ filters,
1432
+ order_by: orderBy,
1433
+ pagination,
1434
+ });
1435
+ }
1436
+
1437
+ async researchWhoBoughtSold(params = {}) {
1438
+ const { tokenAddress, chain = 'solana', fromDate, toDate, buyOrSell = 'BUY', filters = {}, orderBy, pagination } = params;
1439
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1440
+ return this.request('/api/v1beta1/tgm/historical-who-bought-sold', {
1441
+ token_address: tokenAddress,
1442
+ chain,
1443
+ buy_or_sell: buyOrSell,
1444
+ date_range: { from: fromDate, to: toDate },
1445
+ filters,
1446
+ order_by: orderBy,
1447
+ pagination,
1448
+ });
1449
+ }
1450
+
1451
+ async researchSmartMoneyBalances(params = {}) {
1452
+ // API does not support order_by on this endpoint.
1453
+ const { chains = ['solana'], asOfDate, filters = {}, pagination } = params;
1454
+ return this.request('/api/v1beta1/smart-money/historical-token-balances', {
1455
+ chains,
1456
+ as_of_date: asOfDate,
1457
+ filters,
1458
+ pagination,
1459
+ });
1460
+ }
1461
+
1462
+ async researchTokenScreener(params = {}) {
1463
+ const { chains = ['solana'], timeframeDays, toDate, filters = {}, orderBy, pagination } = params;
1464
+ return this.request('/api/v1beta1/token-screener/historical', {
1465
+ chains,
1466
+ timeframe_days: timeframeDays,
1467
+ to_date: toDate,
1468
+ filters,
1469
+ order_by: orderBy,
1470
+ pagination,
1471
+ });
1472
+ }
1473
+
1474
+ async researchWalletBalances(params = {}) {
1475
+ const { address, chain = 'ethereum', asOfDate, filters = {}, orderBy, pagination } = params;
1476
+ if (address) requireValidAddress(address, chain);
1477
+ return this.request('/api/v1beta1/profiler/address/historical-token-balances', {
1478
+ address,
1479
+ chain,
1480
+ as_of_date: asOfDate,
1481
+ filters,
1482
+ order_by: orderBy,
1483
+ pagination,
1484
+ });
1485
+ }
1486
+
1487
+ async researchTxLookup(params = {}) {
1488
+ const { txHash, chain = 'ethereum', asOfDate, blockTimestamp } = params;
1489
+ const body = {
1490
+ transaction_hash: txHash,
1491
+ chain,
1492
+ as_of_date: asOfDate,
1493
+ };
1494
+ if (blockTimestamp) body.block_timestamp = blockTimestamp;
1495
+ return this.request('/api/v1beta1/profiler/historical-transaction-lookup', body);
1496
+ }
1497
+
1498
+ async researchWalletTransactions(params = {}) {
1499
+ const { address, chain = 'ethereum', asOfDate, filters = {}, orderBy, pagination } = params;
1500
+ if (address) requireValidAddress(address, chain);
1501
+ return this.request('/api/v1beta1/profiler/address/historical-transactions', {
1502
+ address,
1503
+ chain,
1504
+ as_of_date: asOfDate,
1505
+ filters,
1506
+ order_by: orderBy,
1507
+ pagination,
1508
+ });
1509
+ }
1510
+
1366
1511
  // ============= Smart Alert Endpoints =============
1367
1512
 
1368
1513
  async alertsList(params = {}) {
package/src/cli.js CHANGED
@@ -9,6 +9,7 @@ import { buildTradingCommands } from './trading.js';
9
9
  import { buildLimitOrderCommands } from './limit-order.js';
10
10
  import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
11
11
  import { buildAgentCommands } from './commands/agent.js';
12
+ import { buildResearchCommands, RESEARCH_HISTORICAL_SUBCOMMANDS } from './commands/research.js';
12
13
  import { resolveAddress, isEnsName } from './ens.js';
13
14
  import fs from 'fs';
14
15
  import { getUpdateNotification, getUpgradeNotice, scheduleUpdateCheck } from './update-check.js';
@@ -1471,19 +1472,25 @@ export function buildCommands(deps = {}) {
1471
1472
  // 'research' delegates to the category handlers defined above
1472
1473
  const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points', 'prediction-market']);
1473
1474
 
1475
+ const researchHistorical = buildResearchCommands(deps).research;
1476
+
1474
1477
  cmds['research'] = async (args, apiInstance, flags, options) => {
1475
1478
  const rawCategory = args[0];
1476
1479
  if (!rawCategory || rawCategory === 'help') {
1477
1480
  return {
1478
1481
  categories: [...RESEARCH_CATEGORIES],
1482
+ historical: [...RESEARCH_HISTORICAL_SUBCOMMANDS],
1479
1483
  aliases: RESEARCH_CATEGORY_ALIASES,
1480
1484
  description: 'Research and analytics commands',
1481
1485
  example: 'nansen research smart-money netflow --chain solana'
1482
1486
  };
1483
1487
  }
1488
+ if (RESEARCH_HISTORICAL_SUBCOMMANDS.has(rawCategory)) {
1489
+ return researchHistorical(args, apiInstance, flags, options);
1490
+ }
1484
1491
  const category = RESEARCH_CATEGORY_ALIASES[rawCategory] || rawCategory;
1485
1492
  if (!RESEARCH_CATEGORIES.has(category)) {
1486
- throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES].join(', ')}`, ErrorCode.UNKNOWN);
1493
+ throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES, ...RESEARCH_HISTORICAL_SUBCOMMANDS].join(', ')}`, ErrorCode.UNKNOWN);
1487
1494
  }
1488
1495
  return cmds[category](args.slice(1), apiInstance, flags, options);
1489
1496
  };
@@ -1712,6 +1719,19 @@ export async function runCLI(rawArgs, deps = {}) {
1712
1719
  if (catSchema.subcommands) {
1713
1720
  lines.push('Subcommands: ' + Object.keys(catSchema.subcommands).join(', '));
1714
1721
  lines.push(`Use: nansen research ${category} <subcommand> --help`);
1722
+ } else if (catSchema.options) {
1723
+ // Leaf historical subcommand: render options + example
1724
+ const params = Object.entries(catSchema.options).map(([name, opt]) => {
1725
+ const parts = [`--${name}`];
1726
+ if (opt.required) parts[0] += '*';
1727
+ if (opt.default !== undefined) parts.push(`(${opt.default})`);
1728
+ return parts.join(' ');
1729
+ });
1730
+ lines.push(`Params (* required): ${params.join(', ')}`);
1731
+ if (catSchema.endpoint) {
1732
+ const cost = getCostForEndpoint(catSchema.endpoint);
1733
+ if (cost) lines.push(`Cost: ${cost.free} credit${cost.free === 1 ? '' : 's'} (Free tier) / ${cost.pro} credit${cost.pro === 1 ? '' : 's'} (Pro tier)`);
1734
+ }
1715
1735
  }
1716
1736
  output(lines.join('\n'));
1717
1737
  notify();
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Nansen CLI - Research command
3
+ *
4
+ * Historical/point-in-time analytics. Each subcommand resolves labels and
5
+ * metrics at the requested date rather than current state — useful for
6
+ * backtesting and historical research.
7
+ */
8
+
9
+ import { NansenError, ErrorCode } from '../api.js';
10
+
11
+ // Local copies of CLI helpers to avoid a circular import with src/cli.js.
12
+ function buildPagination(options) {
13
+ if (!options.limit && !options.page) return undefined;
14
+ return {
15
+ page: Math.max(1, parseInt(options.page, 10) || 1),
16
+ per_page: options.limit,
17
+ };
18
+ }
19
+
20
+ function parseSort(sortOption, orderByOption) {
21
+ if (orderByOption) return orderByOption;
22
+ if (!sortOption) return undefined;
23
+ const parts = String(sortOption).split(':');
24
+ const field = parts[0];
25
+ const direction = (parts[1] || 'desc').toUpperCase();
26
+ return [{ field, direction }];
27
+ }
28
+
29
+ const SUBCOMMANDS = [
30
+ 'historical-dex-trades',
31
+ 'historical-pnl-leaderboard',
32
+ 'historical-token-flow-summary',
33
+ 'historical-token-quant-scores',
34
+ 'historical-top-holders',
35
+ 'historical-who-bought-sold',
36
+ 'historical-smart-money-balances',
37
+ 'historical-token-screener',
38
+ 'historical-wallet-balances',
39
+ 'historical-tx-lookup',
40
+ 'historical-wallet-transactions',
41
+ ];
42
+
43
+ export const RESEARCH_HISTORICAL_SUBCOMMANDS = new Set(SUBCOMMANDS);
44
+
45
+ function requireOptions(options, required) {
46
+ const missing = required.filter(name => !options[name]);
47
+ if (missing.length > 0) {
48
+ throw new NansenError(
49
+ `Required: ${missing.map(n => '--' + n).join(', ')}`,
50
+ ErrorCode.MISSING_PARAM,
51
+ );
52
+ }
53
+ }
54
+
55
+ function resolveDateRange(options) {
56
+ return { fromDate: options['from-date'], toDate: options['to-date'] };
57
+ }
58
+
59
+ function parseTimeframeDays(value) {
60
+ if (value === undefined || value === null || value === '') return undefined;
61
+ const n = parseInt(value, 10);
62
+ if (Number.isNaN(n)) {
63
+ throw new NansenError('--timeframe-days must be an integer', ErrorCode.INVALID_PARAMS);
64
+ }
65
+ return n;
66
+ }
67
+
68
+ function parseChains(options) {
69
+ if (options.chains) {
70
+ return Array.isArray(options.chains)
71
+ ? options.chains
72
+ : String(options.chains).split(',').map(s => s.trim()).filter(Boolean);
73
+ }
74
+ if (options.chain) return [options.chain];
75
+ return undefined;
76
+ }
77
+
78
+ const HELP_TOP = `nansen research — Historical/point-in-time analytics
79
+
80
+ SUBCOMMANDS:
81
+ historical-dex-trades Historical DEX trades for a token
82
+ historical-pnl-leaderboard Historical PnL leaderboard for a token
83
+ historical-token-flow-summary Historical token flow summary
84
+ historical-token-quant-scores Historical token quantitative scores
85
+ historical-top-holders Historical top holders of a token
86
+ historical-who-bought-sold Historical buyers/sellers of a token
87
+ historical-smart-money-balances Historical smart money token balances
88
+ historical-token-screener Historical token screener
89
+ historical-wallet-balances Historical token balances for a wallet
90
+ historical-tx-lookup Lookup a historical transaction by hash
91
+ historical-wallet-transactions Historical transactions for a wallet
92
+
93
+ COMMON OPTIONS:
94
+ --from-date <YYYY-MM-DD> Start of date range (for range-based subcommands)
95
+ --to-date <YYYY-MM-DD> End of date range (for range-based subcommands)
96
+ --as-of-date <YYYY-MM-DD> Snapshot date (for as-of-date subcommands)
97
+ --chain <chain> Chain (default: solana for tokens, ethereum for wallets)
98
+ --page <n> --limit <n> Pagination (not supported by historical-token-flow-summary)
99
+ --sort <field[:asc|desc]> Sort order (not supported by historical-smart-money-balances)
100
+ --filters '<json>' Filters as JSON object
101
+
102
+ Run: nansen research <subcommand> --help`;
103
+
104
+ const SUB_HELP = {
105
+ 'historical-dex-trades': `nansen research historical-dex-trades — Historical DEX trades for a token
106
+
107
+ USAGE:
108
+ nansen research historical-dex-trades --token-address <addr> --from-date <YYYY-MM-DD> --to-date <YYYY-MM-DD> [--chain <chain>]`,
109
+ 'historical-pnl-leaderboard': `nansen research historical-pnl-leaderboard — Historical PnL leaderboard for a token
110
+
111
+ USAGE:
112
+ nansen research historical-pnl-leaderboard --token-address <addr> --from-date <YYYY-MM-DD> --to-date <YYYY-MM-DD> [--chain <chain>]`,
113
+ 'historical-token-flow-summary': `nansen research historical-token-flow-summary — Historical token flow summary
114
+
115
+ USAGE:
116
+ nansen research historical-token-flow-summary --token-address <addr> --from-date <YYYY-MM-DD> --to-date <YYYY-MM-DD> [--chain <chain>]
117
+
118
+ NOTE: This endpoint does not support pagination.`,
119
+ 'historical-token-quant-scores': `nansen research historical-token-quant-scores — Historical token quantitative scores
120
+
121
+ USAGE:
122
+ nansen research historical-token-quant-scores --token-address <addr> --as-of-date <YYYY-MM-DD> [--chain <chain>]`,
123
+ 'historical-top-holders': `nansen research historical-top-holders — Historical top holders of a token
124
+
125
+ USAGE:
126
+ nansen research historical-top-holders --token-address <addr> --as-of-date <YYYY-MM-DD> [--chain <chain>]`,
127
+ 'historical-who-bought-sold': `nansen research historical-who-bought-sold — Historical buyers/sellers of a token
128
+
129
+ USAGE:
130
+ nansen research historical-who-bought-sold --token-address <addr> --from-date <YYYY-MM-DD> --to-date <YYYY-MM-DD> [--buy-or-sell BUY|SELL] [--chain <chain>]`,
131
+ 'historical-smart-money-balances': `nansen research historical-smart-money-balances — Historical smart money token balances
132
+
133
+ USAGE:
134
+ nansen research historical-smart-money-balances --as-of-date <YYYY-MM-DD> [--chains c1,c2]
135
+
136
+ NOTE: This endpoint does not support order_by.`,
137
+ 'historical-token-screener': `nansen research historical-token-screener — Historical token screener
138
+
139
+ USAGE:
140
+ nansen research historical-token-screener --timeframe-days <n> --to-date <YYYY-MM-DD> [--chains c1,c2]`,
141
+ 'historical-wallet-balances': `nansen research historical-wallet-balances — Historical token balances for a wallet
142
+
143
+ USAGE:
144
+ nansen research historical-wallet-balances --address <addr> --as-of-date <YYYY-MM-DD> [--chain <chain>]`,
145
+ 'historical-tx-lookup': `nansen research historical-tx-lookup — Lookup a historical transaction by hash
146
+
147
+ USAGE:
148
+ nansen research historical-tx-lookup --transaction-hash <hash> --as-of-date <YYYY-MM-DD> [--chain <chain>] [--block-timestamp "YYYY-MM-DD HH:MM:SS"]
149
+
150
+ NOTE: Providing --block-timestamp skips a slow hash-resolution step and returns results much faster.`,
151
+ 'historical-wallet-transactions': `nansen research historical-wallet-transactions — Historical transactions for a wallet
152
+
153
+ USAGE:
154
+ nansen research historical-wallet-transactions --address <addr> --as-of-date <YYYY-MM-DD> [--chain <chain>]`,
155
+ };
156
+
157
+ export function buildResearchCommands(deps = {}) {
158
+ const { log = console.log } = deps;
159
+
160
+ return {
161
+ 'research': async (args, apiInstance, flags, options) => {
162
+ const sub = args[0];
163
+
164
+ if (!sub || sub === 'help') {
165
+ log(HELP_TOP);
166
+ return;
167
+ }
168
+
169
+ if (!RESEARCH_HISTORICAL_SUBCOMMANDS.has(sub)) {
170
+ throw new NansenError(
171
+ `Unknown research subcommand: ${sub}. Available: ${SUBCOMMANDS.join(', ')}`,
172
+ ErrorCode.UNKNOWN,
173
+ );
174
+ }
175
+
176
+ if (flags.help || flags.h || args[1] === 'help') {
177
+ log(SUB_HELP[sub] || HELP_TOP);
178
+ return;
179
+ }
180
+
181
+ const orderBy = parseSort(options.sort, options['order-by']);
182
+ const pagination = buildPagination(options);
183
+ const filters = options.filters || {};
184
+ const { fromDate, toDate } = resolveDateRange(options);
185
+ const asOfDate = options['as-of-date'];
186
+
187
+ // Range-based token endpoints (require --from-date + --to-date)
188
+ const rangeTokenHandlers = {
189
+ 'historical-dex-trades': () => apiInstance.researchDexTrades({
190
+ tokenAddress: options['token-address'] || options.token,
191
+ chain: options.chain,
192
+ fromDate, toDate, filters, orderBy, pagination,
193
+ }),
194
+ 'historical-pnl-leaderboard': () => apiInstance.researchPnlLeaderboard({
195
+ tokenAddress: options['token-address'] || options.token,
196
+ chain: options.chain,
197
+ fromDate, toDate, filters, orderBy, pagination,
198
+ }),
199
+ 'historical-token-flow-summary': () => apiInstance.researchTokenFlowSummary({
200
+ tokenAddress: options['token-address'] || options.token,
201
+ chain: options.chain,
202
+ fromDate, toDate, filters, orderBy,
203
+ }),
204
+ 'historical-who-bought-sold': () => apiInstance.researchWhoBoughtSold({
205
+ tokenAddress: options['token-address'] || options.token,
206
+ chain: options.chain,
207
+ buyOrSell: options['buy-or-sell'],
208
+ fromDate, toDate, filters, orderBy, pagination,
209
+ }),
210
+ };
211
+
212
+ if (rangeTokenHandlers[sub]) {
213
+ requireOptions(
214
+ { 'token-address': options['token-address'] || options.token, 'from-date': fromDate, 'to-date': toDate },
215
+ ['token-address', 'from-date', 'to-date'],
216
+ );
217
+ if (sub === 'historical-token-flow-summary' && (options.page || options.limit)) {
218
+ throw new NansenError(
219
+ 'historical-token-flow-summary does not support --page or --limit (endpoint returns a single aggregated row)',
220
+ ErrorCode.INVALID_PARAMS,
221
+ );
222
+ }
223
+ return rangeTokenHandlers[sub]();
224
+ }
225
+
226
+ // As-of-date token endpoints (require --as-of-date)
227
+ const asOfTokenHandlers = {
228
+ 'historical-token-quant-scores': () => apiInstance.researchTokenQuantScores({
229
+ tokenAddress: options['token-address'] || options.token,
230
+ chain: options.chain,
231
+ asOfDate, filters, orderBy, pagination,
232
+ }),
233
+ 'historical-top-holders': () => apiInstance.researchTopHolders({
234
+ tokenAddress: options['token-address'] || options.token,
235
+ chain: options.chain,
236
+ asOfDate, filters, orderBy, pagination,
237
+ }),
238
+ };
239
+
240
+ if (asOfTokenHandlers[sub]) {
241
+ requireOptions(
242
+ { 'token-address': options['token-address'] || options.token, 'as-of-date': asOfDate },
243
+ ['token-address', 'as-of-date'],
244
+ );
245
+ return asOfTokenHandlers[sub]();
246
+ }
247
+
248
+ if (sub === 'historical-smart-money-balances') {
249
+ if (options.sort || options['order-by']) {
250
+ throw new NansenError(
251
+ 'historical-smart-money-balances does not support --sort or --order-by (endpoint does not support ordering)',
252
+ ErrorCode.INVALID_PARAMS,
253
+ );
254
+ }
255
+ requireOptions({ 'as-of-date': asOfDate }, ['as-of-date']);
256
+ return apiInstance.researchSmartMoneyBalances({
257
+ chains: parseChains(options),
258
+ asOfDate, filters, pagination,
259
+ });
260
+ }
261
+
262
+ if (sub === 'historical-token-screener') {
263
+ const timeframeDays = parseTimeframeDays(options['timeframe-days']);
264
+ requireOptions({ 'timeframe-days': timeframeDays, 'to-date': options['to-date'] }, ['timeframe-days', 'to-date']);
265
+ return apiInstance.researchTokenScreener({
266
+ chains: parseChains(options),
267
+ timeframeDays,
268
+ toDate: options['to-date'],
269
+ filters, orderBy, pagination,
270
+ });
271
+ }
272
+
273
+ if (sub === 'historical-wallet-balances') {
274
+ requireOptions(
275
+ { address: options.address, 'as-of-date': asOfDate },
276
+ ['address', 'as-of-date'],
277
+ );
278
+ return apiInstance.researchWalletBalances({
279
+ address: options.address,
280
+ chain: options.chain,
281
+ asOfDate, filters, orderBy, pagination,
282
+ });
283
+ }
284
+
285
+ if (sub === 'historical-tx-lookup') {
286
+ requireOptions(
287
+ { 'transaction-hash': options['transaction-hash'], 'as-of-date': asOfDate },
288
+ ['transaction-hash', 'as-of-date'],
289
+ );
290
+ return apiInstance.researchTxLookup({
291
+ txHash: options['transaction-hash'],
292
+ chain: options.chain,
293
+ asOfDate,
294
+ blockTimestamp: options['block-timestamp'],
295
+ });
296
+ }
297
+
298
+ if (sub === 'historical-wallet-transactions') {
299
+ requireOptions(
300
+ { address: options.address, 'as-of-date': asOfDate },
301
+ ['address', 'as-of-date'],
302
+ );
303
+ return apiInstance.researchWalletTransactions({
304
+ address: options.address,
305
+ chain: options.chain,
306
+ asOfDate, filters, orderBy, pagination,
307
+ });
308
+ }
309
+
310
+ // Should be unreachable because SUBCOMMANDS guarded above.
311
+ throw new NansenError(`Unknown research subcommand: ${sub}`, ErrorCode.UNKNOWN);
312
+ },
313
+ };
314
+ }
package/src/schema.json CHANGED
@@ -697,6 +697,110 @@
697
697
  "description": "Points leaderboard"
698
698
  }
699
699
  }
700
+ },
701
+ "historical-dex-trades": {
702
+ "endpoint": "/api/v1beta1/tgm/historical-dex-trades",
703
+ "description": "Historical DEX trades for a token at a point in time",
704
+ "options": {
705
+ "token-address": { "required": true, "description": "Token address" },
706
+ "from-date": { "required": true, "description": "Start of date range (YYYY-MM-DD)" },
707
+ "to-date": { "required": true, "description": "End of date range (YYYY-MM-DD)" },
708
+ "chain": { "default": "solana", "description": "Chain" }
709
+ }
710
+ },
711
+ "historical-pnl-leaderboard": {
712
+ "endpoint": "/api/v1beta1/tgm/historical-pnl-leaderboard",
713
+ "description": "Historical PnL leaderboard for a token",
714
+ "options": {
715
+ "token-address": { "required": true, "description": "Token address" },
716
+ "from-date": { "required": true, "description": "Start of date range (YYYY-MM-DD)" },
717
+ "to-date": { "required": true, "description": "End of date range (YYYY-MM-DD)" },
718
+ "chain": { "default": "solana", "description": "Chain" }
719
+ }
720
+ },
721
+ "historical-token-flow-summary": {
722
+ "endpoint": "/api/v1beta1/tgm/historical-token-flow-summary",
723
+ "description": "Historical token flow summary (no pagination)",
724
+ "options": {
725
+ "token-address": { "required": true, "description": "Token address" },
726
+ "from-date": { "required": true, "description": "Start of date range (YYYY-MM-DD)" },
727
+ "to-date": { "required": true, "description": "End of date range (YYYY-MM-DD)" },
728
+ "chain": { "default": "solana", "description": "Chain" }
729
+ }
730
+ },
731
+ "historical-token-quant-scores": {
732
+ "endpoint": "/api/v1beta1/tgm/historical-token-quant-scores",
733
+ "description": "Historical token quantitative scores at a snapshot date",
734
+ "options": {
735
+ "token-address": { "required": true, "description": "Token address" },
736
+ "as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
737
+ "chain": { "default": "solana", "description": "Chain" }
738
+ }
739
+ },
740
+ "historical-top-holders": {
741
+ "endpoint": "/api/v1beta1/tgm/historical-top-holders",
742
+ "description": "Historical top holders of a token at a snapshot date",
743
+ "options": {
744
+ "token-address": { "required": true, "description": "Token address" },
745
+ "as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
746
+ "chain": { "default": "solana", "description": "Chain" }
747
+ }
748
+ },
749
+ "historical-who-bought-sold": {
750
+ "endpoint": "/api/v1beta1/tgm/historical-who-bought-sold",
751
+ "description": "Historical buyers/sellers of a token",
752
+ "options": {
753
+ "token-address": { "required": true, "description": "Token address" },
754
+ "from-date": { "required": true, "description": "Start of date range (YYYY-MM-DD)" },
755
+ "to-date": { "required": true, "description": "End of date range (YYYY-MM-DD)" },
756
+ "buy-or-sell": { "default": "BUY", "description": "BUY or SELL" },
757
+ "chain": { "default": "solana", "description": "Chain" }
758
+ }
759
+ },
760
+ "historical-smart-money-balances": {
761
+ "endpoint": "/api/v1beta1/smart-money/historical-token-balances",
762
+ "description": "Historical smart money token balances at a snapshot date (no order_by)",
763
+ "options": {
764
+ "as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
765
+ "chains": { "default": "solana", "description": "Comma-separated chains" }
766
+ }
767
+ },
768
+ "historical-token-screener": {
769
+ "endpoint": "/api/v1beta1/token-screener/historical",
770
+ "description": "Historical token screener over a trailing window",
771
+ "options": {
772
+ "timeframe-days": { "required": true, "type": "number", "description": "Trailing window size in days" },
773
+ "to-date": { "required": true, "description": "End date for the window (YYYY-MM-DD)" },
774
+ "chains": { "default": "solana", "description": "Comma-separated chains" }
775
+ }
776
+ },
777
+ "historical-wallet-balances": {
778
+ "endpoint": "/api/v1beta1/profiler/address/historical-token-balances",
779
+ "description": "Historical token balances for a wallet at a snapshot date",
780
+ "options": {
781
+ "address": { "required": true, "description": "Wallet address" },
782
+ "as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
783
+ "chain": { "default": "ethereum", "description": "Chain" }
784
+ }
785
+ },
786
+ "historical-tx-lookup": {
787
+ "endpoint": "/api/v1beta1/profiler/historical-transaction-lookup",
788
+ "description": "Lookup a historical transaction by hash",
789
+ "options": {
790
+ "transaction-hash": { "required": true, "description": "Transaction hash (0x-prefixed, 66 chars)" },
791
+ "as-of-date": { "required": true, "description": "Reference date for label and pricing resolution (YYYY-MM-DD)" },
792
+ "block-timestamp": { "description": "Block timestamp (YYYY-MM-DD HH:MM:SS) — skips slow hash-resolution step if provided" },
793
+ "chain": { "default": "ethereum", "description": "Chain (ethereum, bnb, base)" }
794
+ }
795
+ },
796
+ "historical-wallet-transactions": {
797
+ "endpoint": "/api/v1beta1/profiler/address/historical-transactions",
798
+ "description": "Historical transactions for a wallet at a snapshot date",
799
+ "options": {
800
+ "address": { "required": true, "description": "Wallet address" },
801
+ "as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
802
+ "chain": { "default": "ethereum", "description": "Chain" }
803
+ }
700
804
  }
701
805
  }
702
806
  },
package/src/trading.js CHANGED
@@ -159,6 +159,7 @@ export async function getQuote(params) {
159
159
  * @param {object} params
160
160
  * @param {string} params.signedTransaction - Base64 (Solana) or 0x hex (EVM)
161
161
  * @param {string} [params.chain] - Target chain name
162
+ * @param {string} [params.quoteId] - Backend quote ID for BI correlation
162
163
  * @param {string} [params.requestId] - Optional Jupiter request ID (Solana only)
163
164
  * @param {boolean} [params.simulate] - Run pre-broadcast simulation
164
165
  * @returns {Promise<object>} Execution result
@@ -2018,16 +2019,19 @@ EXAMPLES:
2018
2019
  simulate: !noSimulate && !gasless,
2019
2020
  };
2020
2021
 
2021
- // Include backend quoteId if available for better BI tracking
2022
- if (currentQuote.metadata?.quoteId) {
2023
- execParams.quoteId = currentQuote.metadata.quoteId;
2022
+ // Prefer the backend quote id saved from /quote; per-aggregator ids can
2023
+ // appear on individual quote metadata and are only a fallback.
2024
+ const backendQuoteId =
2025
+ quoteData.response?.metadata?.quoteId ?? currentQuote.metadata?.quoteId;
2026
+ if (backendQuoteId) {
2027
+ execParams.quoteId = backendQuoteId;
2024
2028
  }
2025
2029
  // The backend's /execute schema is strict; sending fields it doesn't expect
2026
2030
  // for the (chain × aggregator × gasless) combination causes 502s or
2027
2031
  // "Unrecognized keys" rejections. The matrix we've validated against the
2028
2032
  // live backend:
2029
- // - EVM signed (any aggregator): no extra fields. requestId/aggregator
2030
- // trigger schema errors.
2033
+ // - EVM signed (any aggregator): no aggregator/requestId fields.
2034
+ // Those trigger schema errors.
2031
2035
  // - Solana signed (Jupiter/OKX): include requestId for Jupiter Ultra
2032
2036
  // intent resolution.
2033
2037
  // - Solana signed (Relay): omit requestId — backend tries to look it up