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.
- package/CHANGELOG.md +53 -0
- package/package.json +2 -1
- package/skills/nansen-alerts/SKILL.md +137 -0
- package/skills/nansen-alpha-discovery/SKILL.md +43 -0
- package/skills/nansen-batch-wallet/SKILL.md +26 -0
- package/skills/nansen-cross-chain-flow/SKILL.md +27 -0
- package/skills/nansen-dca-watch/SKILL.md +38 -0
- package/skills/nansen-defi-exposure/SKILL.md +37 -0
- package/skills/nansen-exit-signal/SKILL.md +39 -0
- package/skills/nansen-fund-watch/SKILL.md +35 -0
- package/skills/nansen-holder-quality/SKILL.md +38 -0
- package/skills/nansen-perp-scan/SKILL.md +32 -0
- package/skills/nansen-perp-trader/SKILL.md +39 -0
- package/skills/nansen-pm-deep-dive/SKILL.md +50 -0
- package/skills/nansen-pm-insider-scan/SKILL.md +62 -0
- package/skills/nansen-polymarket-trader/SKILL.md +43 -0
- package/skills/nansen-portfolio-history/SKILL.md +36 -0
- package/skills/nansen-prediction-market/SKILL.md +47 -0
- package/skills/nansen-profiler/SKILL.md +98 -0
- package/skills/nansen-search/SKILL.md +34 -0
- package/skills/nansen-sm-trend/SKILL.md +30 -0
- package/skills/nansen-smart-money/SKILL.md +71 -0
- package/skills/nansen-token/SKILL.md +90 -0
- package/skills/nansen-token-discovery/SKILL.md +54 -0
- package/skills/nansen-token-forensics/SKILL.md +40 -0
- package/skills/nansen-trade/SKILL.md +100 -0
- package/skills/nansen-wallet/SKILL.md +140 -0
- package/skills/nansen-wallet-analysis/SKILL.md +45 -0
- package/skills/nansen-wallet-attribution/REFERENCE.md +43 -0
- package/skills/nansen-wallet-attribution/SKILL.md +46 -0
- package/skills/nansen-wallet-migration/SKILL.md +183 -0
- package/skills/nansen-web-fetch/SKILL.md +50 -0
- package/skills/nansen-web-search/SKILL.md +39 -0
- package/src/api.js +144 -73
- package/src/cli.js +181 -14
- package/src/rpc-urls.js +29 -0
- package/src/schema.json +401 -1448
- package/src/telemetry.js +237 -0
- package/src/trading.js +26 -12
- package/src/transfer.js +1 -11
- package/src/update-check.js +2 -2
- package/src/wallet.js +2 -1
- 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
|
|
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
|
|
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.
|
|
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
|
|
528
|
-
|
|
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
|
|
561
|
-
|
|
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
|
|
608
|
-
|
|
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;
|