nansen-cli 1.16.1 → 1.18.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/package.json +2 -1
  3. package/skills/nansen-alerts/SKILL.md +137 -0
  4. package/skills/nansen-alpha-discovery/SKILL.md +43 -0
  5. package/skills/nansen-batch-wallet/SKILL.md +26 -0
  6. package/skills/nansen-cross-chain-flow/SKILL.md +27 -0
  7. package/skills/nansen-dca-watch/SKILL.md +38 -0
  8. package/skills/nansen-defi-exposure/SKILL.md +37 -0
  9. package/skills/nansen-exit-signal/SKILL.md +39 -0
  10. package/skills/nansen-fund-watch/SKILL.md +35 -0
  11. package/skills/nansen-holder-quality/SKILL.md +38 -0
  12. package/skills/nansen-perp-scan/SKILL.md +32 -0
  13. package/skills/nansen-perp-trader/SKILL.md +39 -0
  14. package/skills/nansen-pm-deep-dive/SKILL.md +50 -0
  15. package/skills/nansen-pm-insider-scan/SKILL.md +62 -0
  16. package/skills/nansen-polymarket-trader/SKILL.md +43 -0
  17. package/skills/nansen-portfolio-history/SKILL.md +36 -0
  18. package/skills/nansen-prediction-market/SKILL.md +47 -0
  19. package/skills/nansen-profiler/SKILL.md +98 -0
  20. package/skills/nansen-search/SKILL.md +34 -0
  21. package/skills/nansen-sm-trend/SKILL.md +30 -0
  22. package/skills/nansen-smart-money/SKILL.md +71 -0
  23. package/skills/nansen-token/SKILL.md +90 -0
  24. package/skills/nansen-token-discovery/SKILL.md +54 -0
  25. package/skills/nansen-token-forensics/SKILL.md +40 -0
  26. package/skills/nansen-trade/SKILL.md +100 -0
  27. package/skills/nansen-wallet/SKILL.md +140 -0
  28. package/skills/nansen-wallet-analysis/SKILL.md +45 -0
  29. package/skills/nansen-wallet-attribution/REFERENCE.md +43 -0
  30. package/skills/nansen-wallet-attribution/SKILL.md +46 -0
  31. package/skills/nansen-wallet-migration/SKILL.md +183 -0
  32. package/skills/nansen-web-fetch/SKILL.md +50 -0
  33. package/skills/nansen-web-search/SKILL.md +39 -0
  34. package/src/api.js +144 -73
  35. package/src/cli.js +181 -14
  36. package/src/rpc-urls.js +29 -0
  37. package/src/schema.json +401 -1448
  38. package/src/telemetry.js +237 -0
  39. package/src/trading.js +26 -12
  40. package/src/transfer.js +1 -11
  41. package/src/update-check.js +2 -2
  42. package/src/wallet.js +2 -1
  43. package/src/x402.js +3 -2
package/src/api.js CHANGED
@@ -7,9 +7,15 @@ import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { EVM_CHAINS } from './chain-ids.js';
10
+ import { getAnonymousId, TELEMETRY_DISABLED } from './telemetry.js';
10
11
 
11
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
13
 
14
+ function telemetryHeaders() {
15
+ if (TELEMETRY_DISABLED) return {};
16
+ return { 'X-Anonymous-Id': getAnonymousId() };
17
+ }
18
+
13
19
  const { version: packageVersion } = JSON.parse(
14
20
  fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
15
21
  );
@@ -418,6 +424,53 @@ export class NansenAPI {
418
424
  );
419
425
  }
420
426
 
427
+ /**
428
+ * Retry a POST request with a payment signature.
429
+ * Returns parsed JSON if the paid request succeeds, or null if still rejected.
430
+ * Logs the payment and warns about low balance when walletLabel and network are given.
431
+ *
432
+ * @param {string} signature - Payment-Signature header value
433
+ * @param {string|null} walletLabel - Display label for logging, e.g. "local wallet alice"
434
+ * @param {string|null} network - x402 network string for balance check, e.g. "eip155:8453"
435
+ * @param {string} url - Request URL
436
+ * @param {object} body - Request body (will be cleaned)
437
+ * @param {object} [options={}] - Request options (may include .headers)
438
+ * @returns {Promise<object|null>} Parsed JSON on success, null if rejected
439
+ *
440
+ * TODO: full fix — extract the entire x402 provider dispatch from request() into
441
+ * an attemptX402Payment() method so adding a new payment provider only requires
442
+ * touching that one method, not hunting inside the retry loop.
443
+ */
444
+ async _x402Retry(signature, walletLabel, network, url, body, options = {}) {
445
+ const paidResponse = await fetch(url, {
446
+ method: 'POST',
447
+ headers: {
448
+ 'Content-Type': 'application/json',
449
+ 'X-Client-Type': 'nansen-cli',
450
+ 'X-Client-Version': packageVersion,
451
+ ...telemetryHeaders(),
452
+ 'Payment-Signature': signature,
453
+ ...this.defaultHeaders,
454
+ ...options.headers,
455
+ },
456
+ body: JSON.stringify(NansenAPI.cleanBody(body)),
457
+ });
458
+ if (!paidResponse.ok) return null;
459
+ if (walletLabel) {
460
+ console.error(`[x402] Paid via ${walletLabel}${network ? ` (${network})` : ''}`);
461
+ }
462
+ if (network) {
463
+ try {
464
+ const { checkX402Balance } = await import('./x402.js');
465
+ const balance = await checkX402Balance(network);
466
+ if (balance !== null && balance < 0.25) {
467
+ console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
468
+ }
469
+ } catch { /* balance check is best-effort */ }
470
+ }
471
+ return await paidResponse.json();
472
+ }
473
+
421
474
  async request(endpoint, body = {}, options = {}) {
422
475
  const url = `${this.baseUrl}${endpoint}`;
423
476
  const { maxRetries, baseDelayMs, maxDelayMs, retryOnStatus } = this.retryOptions;
@@ -439,17 +492,20 @@ export class NansenAPI {
439
492
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
440
493
  let response;
441
494
  try {
495
+ const method = options.method || 'POST';
496
+ const isGet = method === 'GET';
442
497
  response = await fetch(url, {
443
- method: 'POST',
498
+ method,
444
499
  headers: {
445
- 'Content-Type': 'application/json',
500
+ ...(!isGet && { 'Content-Type': 'application/json' }),
446
501
  'X-Client-Type': 'nansen-cli',
447
502
  'X-Client-Version': packageVersion,
503
+ ...telemetryHeaders(),
448
504
  ...(this.apiKey ? { 'apikey': this.apiKey } : {}),
449
505
  ...this.defaultHeaders,
450
506
  ...options.headers
451
507
  },
452
- body: JSON.stringify(NansenAPI.cleanBody(body))
508
+ ...(!isGet && method !== 'DELETE' && { body: JSON.stringify(NansenAPI.cleanBody(body)) })
453
509
  });
454
510
  } catch (err) {
455
511
  // Network-level errors - retry these too
@@ -490,12 +546,20 @@ export class NansenAPI {
490
546
  }
491
547
 
492
548
  if (!response.ok) {
493
- let message = data.message || data.error || `API error: ${response.status}`;
549
+ let message = data.message || data.error
550
+ || (typeof data.detail === 'string' ? data.detail : data.detail?.message)
551
+ || `API error: ${response.status}`;
552
+ // nansen-api proxy stringifies nested error dicts via Python str(), producing
553
+ // "{'message': 'actual error', ...}". Extract the inner message if present.
554
+ const nestedMatch = typeof message === 'string' && message.match(/['"]message['"]\s*:\s*['"](.*?)['"]/);
555
+ if (nestedMatch) message = nestedMatch[1];
494
556
  const code = statusToErrorCode(response.status, data);
495
557
  const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));
496
558
 
497
559
  // Enhance messages for specific error codes
498
- if (code === ErrorCode.UNSUPPORTED_FILTER) {
560
+ if (code === ErrorCode.UNAUTHORIZED) {
561
+ message = this.apiKey ? message : 'Not logged in. Run: nansen login';
562
+ } else if (code === ErrorCode.UNSUPPORTED_FILTER) {
499
563
  message = message.replace(/\.+$/, '') + '. This filter is not supported for this token/chain combination. Do not retry.';
500
564
  } else if (code === ErrorCode.CREDITS_EXHAUSTED) {
501
565
  message = message.replace(/\.+$/, '') + '. No retry will help. Check your Nansen dashboard for credit balance.';
@@ -524,29 +588,8 @@ export class NansenAPI {
524
588
  try {
525
589
  const { createPrivyPaymentSignatures } = await import('./privy.js');
526
590
  for await (const { signature, network } of createPrivyPaymentSignatures(response, url)) {
527
- const paidResponse = await fetch(url, {
528
- method: 'POST',
529
- headers: {
530
- 'Content-Type': 'application/json',
531
- 'X-Client-Type': 'nansen-cli',
532
- 'X-Client-Version': packageVersion,
533
- 'Payment-Signature': signature,
534
- ...this.defaultHeaders,
535
- ...options.headers,
536
- },
537
- body: JSON.stringify(NansenAPI.cleanBody(body)),
538
- });
539
- if (paidResponse.ok) {
540
- console.error(`[x402] Paid via Privy wallet ${defaultWalletName} (${network})`);
541
- try {
542
- const { checkX402Balance } = await import('./x402.js');
543
- const balance = await checkX402Balance(network);
544
- if (balance !== null && balance < 0.25) {
545
- console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
546
- }
547
- } catch { /* balance check is best-effort */ }
548
- return await paidResponse.json();
549
- }
591
+ const result = await this._x402Retry(signature, `Privy wallet ${defaultWalletName}`, network, url, body, options);
592
+ if (result !== null) return result;
550
593
  }
551
594
  } catch (privyErr) {
552
595
  message = `x402 Privy payment failed: ${privyErr.message}`;
@@ -557,30 +600,8 @@ export class NansenAPI {
557
600
  try {
558
601
  const { createPaymentSignatures } = await import('./x402.js');
559
602
  for await (const { signature, network } of createPaymentSignatures(response, url)) {
560
- const paidResponse = await fetch(url, {
561
- method: 'POST',
562
- headers: {
563
- 'Content-Type': 'application/json',
564
- 'X-Client-Type': 'nansen-cli',
565
- 'X-Client-Version': packageVersion,
566
- 'Payment-Signature': signature,
567
- ...this.defaultHeaders,
568
- ...options.headers,
569
- },
570
- body: JSON.stringify(NansenAPI.cleanBody(body)),
571
- });
572
- if (paidResponse.ok) {
573
- console.error(`[x402] Paid via local wallet ${defaultWalletName} (${network})`);
574
- // Check remaining balance and warn if low
575
- try {
576
- const { checkX402Balance } = await import('./x402.js');
577
- const balance = await checkX402Balance(network);
578
- if (balance !== null && balance < 0.25) {
579
- console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
580
- }
581
- } catch { /* balance check is best-effort */ }
582
- return await paidResponse.json();
583
- }
603
+ const result = await this._x402Retry(signature, `local wallet ${defaultWalletName}`, network, url, body, options);
604
+ if (result !== null) return result;
584
605
  // This payment option was rejected, try next
585
606
  }
586
607
  } catch { /* local wallet unavailable, try WalletConnect */ }
@@ -604,21 +625,8 @@ export class NansenAPI {
604
625
  try {
605
626
  const { handleX402Payment } = await import('./walletconnect-x402.js');
606
627
  const paymentSignature = await handleX402Payment(paymentRequirements);
607
- const paidResponse = await fetch(url, {
608
- method: 'POST',
609
- headers: {
610
- 'Content-Type': 'application/json',
611
- 'X-Client-Type': 'nansen-cli',
612
- 'X-Client-Version': packageVersion,
613
- 'Payment-Signature': paymentSignature,
614
- ...this.defaultHeaders,
615
- ...options.headers,
616
- },
617
- body: JSON.stringify(NansenAPI.cleanBody(body)),
618
- });
619
- if (paidResponse.ok) {
620
- return await paidResponse.json();
621
- }
628
+ const result = await this._x402Retry(paymentSignature, 'WalletConnect', null, url, body, options);
629
+ if (result !== null) return result;
622
630
  } catch (x402Err) {
623
631
  if (!this.apiKey) {
624
632
  message = 'No API key configured. Two ways to authenticate:\n' +
@@ -674,6 +682,12 @@ export class NansenAPI {
674
682
  throw lastError;
675
683
  }
676
684
 
685
+ // ============= Account Endpoint =============
686
+
687
+ async getAccount() {
688
+ return this.request('/api/v1/account', {}, { method: 'GET', cache: false });
689
+ }
690
+
677
691
  // ============= Smart Money Endpoints =============
678
692
 
679
693
  async smartMoneyNetflow(params = {}) {
@@ -821,6 +835,31 @@ export class NansenAPI {
821
835
  return this.request('/api/v1/search/general', body);
822
836
  }
823
837
 
838
+ async webSearch(params = {}) {
839
+ const { queries, numResults = 10 } = params;
840
+ if (!queries || queries.length === 0) {
841
+ throw new NansenError('At least one query is required', ErrorCode.MISSING_PARAM);
842
+ }
843
+ return this.request('/api/v1/search/web-search', {
844
+ queries,
845
+ num_results: numResults,
846
+ }, { cache: false });
847
+ }
848
+
849
+ async webFetch(params = {}) {
850
+ const { urls, question } = params;
851
+ if (!urls || urls.length === 0) {
852
+ throw new NansenError('At least one URL is required', ErrorCode.MISSING_PARAM);
853
+ }
854
+ if (!question) {
855
+ throw new NansenError('A question is required', ErrorCode.MISSING_PARAM);
856
+ }
857
+ return this.request('/api/v1/search/web-fetch', {
858
+ urls,
859
+ question,
860
+ }, { cache: false });
861
+ }
862
+
824
863
  async addressHistoricalBalances(params = {}) {
825
864
  const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
826
865
  if (address) {
@@ -1145,7 +1184,7 @@ export class NansenAPI {
1145
1184
 
1146
1185
  async pmOhlcv(params = {}) {
1147
1186
  const { marketId, sort, pagination } = params;
1148
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1187
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1149
1188
  return this.request('/api/v1/prediction-market/ohlcv', {
1150
1189
  market_id: marketId,
1151
1190
  sort,
@@ -1155,7 +1194,7 @@ export class NansenAPI {
1155
1194
 
1156
1195
  async pmOrderbook(params = {}) {
1157
1196
  const { marketId, pagination } = params;
1158
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1197
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1159
1198
  return this.request('/api/v1/prediction-market/orderbook', {
1160
1199
  market_id: marketId,
1161
1200
  pagination
@@ -1164,7 +1203,7 @@ export class NansenAPI {
1164
1203
 
1165
1204
  async pmTopHolders(params = {}) {
1166
1205
  const { marketId, sort, pagination } = params;
1167
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1206
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1168
1207
  return this.request('/api/v1/prediction-market/top-holders', {
1169
1208
  market_id: marketId,
1170
1209
  sort,
@@ -1174,7 +1213,7 @@ export class NansenAPI {
1174
1213
 
1175
1214
  async pmTradesByMarket(params = {}) {
1176
1215
  const { marketId, pagination } = params;
1177
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1216
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1178
1217
  return this.request('/api/v1/prediction-market/trades-by-market', {
1179
1218
  market_id: marketId,
1180
1219
  pagination
@@ -1214,7 +1253,7 @@ export class NansenAPI {
1214
1253
 
1215
1254
  async pmPnlByMarket(params = {}) {
1216
1255
  const { marketId, pagination } = params;
1217
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1256
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1218
1257
  return this.request('/api/v1/prediction-market/pnl-by-market', {
1219
1258
  market_id: marketId,
1220
1259
  pagination
@@ -1234,7 +1273,7 @@ export class NansenAPI {
1234
1273
 
1235
1274
  async pmPositionDetail(params = {}) {
1236
1275
  const { marketId, pagination } = params;
1237
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1276
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1238
1277
  return this.request('/api/v1/prediction-market/position-detail', {
1239
1278
  market_id: marketId,
1240
1279
  pagination
@@ -1266,6 +1305,38 @@ export class NansenAPI {
1266
1305
  wallet_address: walletAddress
1267
1306
  });
1268
1307
  }
1308
+
1309
+ // ============= Smart Alert Endpoints =============
1310
+
1311
+ async alertsList(params = {}) {
1312
+ const defined = Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined));
1313
+ const qs = Object.keys(defined).length > 0 ? '?' + new URLSearchParams(defined).toString() : '';
1314
+ return this.request(`/api/v1/smart-alert/list${qs}`, {}, { method: 'GET' });
1315
+ }
1316
+
1317
+ async alertsCreate(params = {}) {
1318
+ return this.request('/api/v1/smart-alert', params);
1319
+ }
1320
+
1321
+ async alertsUpdate(params = {}) {
1322
+ return this.request('/api/v1/smart-alert', params, { method: 'PATCH' });
1323
+ }
1324
+
1325
+ async alertsToggle(params = {}) {
1326
+ return this.request('/api/v1/smart-alert/toggle', params, { method: 'PATCH' });
1327
+ }
1328
+
1329
+ async alertsGet(id) {
1330
+ // TODO: replace with GET /api/v1/smart-alert/{id} once a get-by-id endpoint exists.
1331
+ // Fetching the full list does not scale for users with many alerts.
1332
+ const result = await this.alertsList();
1333
+ const alerts = Array.isArray(result) ? result : result?.alerts ?? result?.data ?? [];
1334
+ return alerts.find(a => a.id === id) ?? null;
1335
+ }
1336
+
1337
+ async alertsDelete(alertId) {
1338
+ return this.request(`/api/v1/smart-alert/${encodeURIComponent(alertId)}`, {}, { method: 'DELETE' });
1339
+ }
1269
1340
  }
1270
1341
 
1271
1342
  export default NansenAPI;