nansen-cli 1.16.0 → 1.17.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,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#279](https://github.com/nansen-ai/nansen-cli/pull/279) [`174a3d6`](https://github.com/nansen-ai/nansen-cli/commit/174a3d612b3f198c5e5b979c269d896f9d906704) Thanks [@kome12](https://github.com/kome12)! - Add `nansen account` command to verify API key and check credit balance
8
+
9
+ Users can now run `nansen account` to confirm their API key is valid and see
10
+ their current plan and remaining credits — without consuming any credits.
11
+
12
+ This calls the new `GET /api/v1/account` endpoint (ECINT-6365).
13
+
14
+ - [#234](https://github.com/nansen-ai/nansen-cli/pull/234) [`10a5ced`](https://github.com/nansen-ai/nansen-cli/commit/10a5ced9f1f751666d034cecfc431e335183741c) Thanks [@kome12](https://github.com/kome12)! - Reduced schema.json to a minimal format (~66% smaller).
15
+
16
+ ### Patch Changes
17
+
18
+ - [#272](https://github.com/nansen-ai/nansen-cli/pull/272) [`50213c1`](https://github.com/nansen-ai/nansen-cli/commit/50213c1d82375a153aa1bad3a53bbd7059cd9f5b) Thanks [@TimNooren](https://github.com/TimNooren)! - fix: show human-readable error when trade fails due to insufficient ETH
19
+
20
+ When a wallet has no ETH and a trade is attempted, the raw Ethereum RPC
21
+ error ("insufficient funds for gas \* price + value: ... have 0 want
22
+ 400000000000000 (supplied gas 600000000)") is now translated into a
23
+ user-friendly message showing amounts in ETH with a funding hint, e.g.
24
+ "Insufficient ETH: wallet has 0.000000 ETH but this trade needs ~0.000400
25
+ ETH (amount + gas). Send ETH to 0x... before trading."
26
+
27
+ ## 1.16.1
28
+
29
+ ### Patch Changes
30
+
31
+ - [#249](https://github.com/nansen-ai/nansen-cli/pull/249) [`0c17437`](https://github.com/nansen-ai/nansen-cli/commit/0c17437cf65d8b4f0516ede386adefec57d8ab3d) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Add `pm` to top-level COMMAND_ALIASES so `nansen pm <subcommand>` works (previously only `nansen research pm <subcommand>` resolved the alias)
32
+
33
+ - [#244](https://github.com/nansen-ai/nansen-cli/pull/244) [`6427a9f`](https://github.com/nansen-ai/nansen-cli/commit/6427a9fdb7295dee94d7aed10cdb0164c46c7d73) Thanks [@Nicolai1205](https://github.com/Nicolai1205)! - Add 7 new agent skills: nansen-token-search, nansen-sm-trend, nansen-wallet-cluster, nansen-wallet-compare, nansen-token-indicators, nansen-cross-chain-flow, nansen-batch-wallet. All validated against live API.
34
+
3
35
  ## 1.16.0
4
36
 
5
37
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/api.js CHANGED
@@ -418,6 +418,52 @@ export class NansenAPI {
418
418
  );
419
419
  }
420
420
 
421
+ /**
422
+ * Retry a POST request with a payment signature.
423
+ * Returns parsed JSON if the paid request succeeds, or null if still rejected.
424
+ * Logs the payment and warns about low balance when walletLabel and network are given.
425
+ *
426
+ * @param {string} signature - Payment-Signature header value
427
+ * @param {string|null} walletLabel - Display label for logging, e.g. "local wallet alice"
428
+ * @param {string|null} network - x402 network string for balance check, e.g. "eip155:8453"
429
+ * @param {string} url - Request URL
430
+ * @param {object} body - Request body (will be cleaned)
431
+ * @param {object} [options={}] - Request options (may include .headers)
432
+ * @returns {Promise<object|null>} Parsed JSON on success, null if rejected
433
+ *
434
+ * TODO: full fix — extract the entire x402 provider dispatch from request() into
435
+ * an attemptX402Payment() method so adding a new payment provider only requires
436
+ * touching that one method, not hunting inside the retry loop.
437
+ */
438
+ async _x402Retry(signature, walletLabel, network, url, body, options = {}) {
439
+ const paidResponse = await fetch(url, {
440
+ method: 'POST',
441
+ headers: {
442
+ 'Content-Type': 'application/json',
443
+ 'X-Client-Type': 'nansen-cli',
444
+ 'X-Client-Version': packageVersion,
445
+ 'Payment-Signature': signature,
446
+ ...this.defaultHeaders,
447
+ ...options.headers,
448
+ },
449
+ body: JSON.stringify(NansenAPI.cleanBody(body)),
450
+ });
451
+ if (!paidResponse.ok) return null;
452
+ if (walletLabel) {
453
+ console.error(`[x402] Paid via ${walletLabel}${network ? ` (${network})` : ''}`);
454
+ }
455
+ if (network) {
456
+ try {
457
+ const { checkX402Balance } = await import('./x402.js');
458
+ const balance = await checkX402Balance(network);
459
+ if (balance !== null && balance < 0.25) {
460
+ console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
461
+ }
462
+ } catch { /* balance check is best-effort */ }
463
+ }
464
+ return await paidResponse.json();
465
+ }
466
+
421
467
  async request(endpoint, body = {}, options = {}) {
422
468
  const url = `${this.baseUrl}${endpoint}`;
423
469
  const { maxRetries, baseDelayMs, maxDelayMs, retryOnStatus } = this.retryOptions;
@@ -439,17 +485,19 @@ export class NansenAPI {
439
485
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
440
486
  let response;
441
487
  try {
488
+ const method = options.method || 'POST';
489
+ const isGet = method === 'GET';
442
490
  response = await fetch(url, {
443
- method: 'POST',
491
+ method,
444
492
  headers: {
445
- 'Content-Type': 'application/json',
493
+ ...(!isGet && { 'Content-Type': 'application/json' }),
446
494
  'X-Client-Type': 'nansen-cli',
447
495
  'X-Client-Version': packageVersion,
448
496
  ...(this.apiKey ? { 'apikey': this.apiKey } : {}),
449
497
  ...this.defaultHeaders,
450
498
  ...options.headers
451
499
  },
452
- body: JSON.stringify(NansenAPI.cleanBody(body))
500
+ ...(!isGet && { body: JSON.stringify(NansenAPI.cleanBody(body)) })
453
501
  });
454
502
  } catch (err) {
455
503
  // Network-level errors - retry these too
@@ -524,29 +572,8 @@ export class NansenAPI {
524
572
  try {
525
573
  const { createPrivyPaymentSignatures } = await import('./privy.js');
526
574
  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
- }
575
+ const result = await this._x402Retry(signature, `Privy wallet ${defaultWalletName}`, network, url, body, options);
576
+ if (result !== null) return result;
550
577
  }
551
578
  } catch (privyErr) {
552
579
  message = `x402 Privy payment failed: ${privyErr.message}`;
@@ -557,30 +584,8 @@ export class NansenAPI {
557
584
  try {
558
585
  const { createPaymentSignatures } = await import('./x402.js');
559
586
  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
- }
587
+ const result = await this._x402Retry(signature, `local wallet ${defaultWalletName}`, network, url, body, options);
588
+ if (result !== null) return result;
584
589
  // This payment option was rejected, try next
585
590
  }
586
591
  } catch { /* local wallet unavailable, try WalletConnect */ }
@@ -604,21 +609,8 @@ export class NansenAPI {
604
609
  try {
605
610
  const { handleX402Payment } = await import('./walletconnect-x402.js');
606
611
  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
- }
612
+ const result = await this._x402Retry(paymentSignature, 'WalletConnect', null, url, body, options);
613
+ if (result !== null) return result;
622
614
  } catch (x402Err) {
623
615
  if (!this.apiKey) {
624
616
  message = 'No API key configured. Two ways to authenticate:\n' +
@@ -674,6 +666,12 @@ export class NansenAPI {
674
666
  throw lastError;
675
667
  }
676
668
 
669
+ // ============= Account Endpoint =============
670
+
671
+ async getAccount() {
672
+ return this.request('/api/v1/account', {}, { method: 'GET', cache: false });
673
+ }
674
+
677
675
  // ============= Smart Money Endpoints =============
678
676
 
679
677
  async smartMoneyNetflow(params = {}) {
@@ -1145,7 +1143,7 @@ export class NansenAPI {
1145
1143
 
1146
1144
  async pmOhlcv(params = {}) {
1147
1145
  const { marketId, sort, pagination } = params;
1148
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1146
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1149
1147
  return this.request('/api/v1/prediction-market/ohlcv', {
1150
1148
  market_id: marketId,
1151
1149
  sort,
@@ -1155,7 +1153,7 @@ export class NansenAPI {
1155
1153
 
1156
1154
  async pmOrderbook(params = {}) {
1157
1155
  const { marketId, pagination } = params;
1158
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1156
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1159
1157
  return this.request('/api/v1/prediction-market/orderbook', {
1160
1158
  market_id: marketId,
1161
1159
  pagination
@@ -1164,7 +1162,7 @@ export class NansenAPI {
1164
1162
 
1165
1163
  async pmTopHolders(params = {}) {
1166
1164
  const { marketId, sort, pagination } = params;
1167
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1165
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1168
1166
  return this.request('/api/v1/prediction-market/top-holders', {
1169
1167
  market_id: marketId,
1170
1168
  sort,
@@ -1174,7 +1172,7 @@ export class NansenAPI {
1174
1172
 
1175
1173
  async pmTradesByMarket(params = {}) {
1176
1174
  const { marketId, pagination } = params;
1177
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1175
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1178
1176
  return this.request('/api/v1/prediction-market/trades-by-market', {
1179
1177
  market_id: marketId,
1180
1178
  pagination
@@ -1214,7 +1212,7 @@ export class NansenAPI {
1214
1212
 
1215
1213
  async pmPnlByMarket(params = {}) {
1216
1214
  const { marketId, pagination } = params;
1217
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1215
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1218
1216
  return this.request('/api/v1/prediction-market/pnl-by-market', {
1219
1217
  market_id: marketId,
1220
1218
  pagination
@@ -1234,7 +1232,7 @@ export class NansenAPI {
1234
1232
 
1235
1233
  async pmPositionDetail(params = {}) {
1236
1234
  const { marketId, pagination } = params;
1237
- if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1235
+ if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1238
1236
  return this.request('/api/v1/prediction-market/position-detail', {
1239
1237
  market_id: marketId,
1240
1238
  pagination
package/src/cli.js CHANGED
@@ -654,8 +654,8 @@ export async function compareWallets(api, params = {}) {
654
654
  shared_counterparties: sharedCpAddrs,
655
655
  shared_tokens: sharedTokens,
656
656
  balances: [
657
- { address: addr1, total_usd: tokens1.reduce((sum, t) => sum + (t.balance_usd || 0), 0) },
658
- { address: addr2, total_usd: tokens2.reduce((sum, t) => sum + (t.balance_usd || 0), 0) },
657
+ { address: addr1, total_usd: tokens1.reduce((sum, t) => sum + (t.value_usd ?? t.balance_usd ?? 0), 0) },
658
+ { address: addr2, total_usd: tokens2.reduce((sum, t) => sum + (t.value_usd ?? t.balance_usd ?? 0), 0) },
659
659
  ],
660
660
  };
661
661
  }
@@ -670,6 +670,7 @@ COMMANDS:
670
670
  research smart-money, profiler, token, search, perp, portfolio, points
671
671
  trade quote, execute
672
672
  wallet create, list, show, export, default, delete, forget-password
673
+ account Show API key status, plan, and remaining credits
673
674
  login Save API key (--api-key <key> or NANSEN_API_KEY env var)
674
675
  logout Remove saved API key
675
676
  schema JSON schema for all commands (use "nansen schema <cmd>" for one)
@@ -757,6 +758,10 @@ export function buildCommands(deps = {}) {
757
758
  } = deps;
758
759
 
759
760
  const cmds = {
761
+ 'account': async (_args, apiInstance, _flags, _options) => {
762
+ return apiInstance.getAccount();
763
+ },
764
+
760
765
  'login': async (args, apiInstance, flags, options) => {
761
766
  if (flags.help || flags.h) {
762
767
  log('nansen login - Save your Nansen API key\n');
@@ -1240,6 +1245,9 @@ export function buildCommands(deps = {}) {
1240
1245
  },
1241
1246
 
1242
1247
  'prediction-market': async (args, apiInstance, flags, options) => {
1248
+ if (Date.now() < new Date('2026-03-16T00:00:00Z').getTime()) {
1249
+ process.stderr.write('⚠️ PnL data for prediction markets is temporarily unavailable while we improve accuracy. We\'ll update once resolved.\n');
1250
+ }
1243
1251
  const subcommand = args[0] || 'help';
1244
1252
  const marketId = options['market-id'];
1245
1253
  const address = options.address;
@@ -1345,7 +1353,8 @@ export const COMMAND_ALIASES = {
1345
1353
  'tgm': 'token', // Token God Mode
1346
1354
  'sm': 'smart-money', // Smart Money
1347
1355
  'prof': 'profiler', // Profiler
1348
- 'port': 'portfolio' // Portfolio
1356
+ 'port': 'portfolio', // Portfolio
1357
+ 'pm': 'prediction-market' // Prediction Market
1349
1358
  };
1350
1359
 
1351
1360
  // Aliases used inside the 'research' namespace
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Single source of truth for chain RPC endpoints.
3
+ *
4
+ * Both trading.js and transfer.js read from here so that:
5
+ * (a) adding a new chain only requires one edit, and
6
+ * (b) env-var overrides work consistently across all commands.
7
+ *
8
+ * Override env vars:
9
+ * NANSEN_EVM_RPC Custom Ethereum RPC (also used as generic EVM fallback)
10
+ * NANSEN_BASE_RPC Custom Base RPC
11
+ * NANSEN_SOLANA_RPC Custom Solana RPC
12
+ *
13
+ * Backward-compat aliases (deprecated — prefer the forms above):
14
+ * NANSEN_RPC_BASE Old name for NANSEN_BASE_RPC; trading.js previously read this
15
+ * but transfer.js never did, so the two commands were inconsistent.
16
+ * Both forms are now accepted here so existing .env files keep
17
+ * working while new code uses the standardised NANSEN_BASE_RPC name.
18
+ */
19
+
20
+ const DEFAULT_EVM_RPC = 'https://eth.public-rpc.com';
21
+ const DEFAULT_BASE_RPC = 'https://mainnet.base.org';
22
+ const DEFAULT_SOLANA_RPC = 'https://api.mainnet-beta.solana.com';
23
+
24
+ export const CHAIN_RPCS = {
25
+ ethereum: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC,
26
+ evm: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC, // generic EVM fallback
27
+ base: process.env.NANSEN_BASE_RPC || process.env.NANSEN_RPC_BASE || DEFAULT_BASE_RPC,
28
+ solana: process.env.NANSEN_SOLANA_RPC || DEFAULT_SOLANA_RPC,
29
+ };