nansen-cli 1.6.0 → 1.8.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/SKILL.md CHANGED
@@ -12,21 +12,40 @@ compatibility: Requires Node.js 18+. Needs NANSEN_API_KEY environment variable o
12
12
 
13
13
  Command-line interface for the [Nansen API](https://docs.nansen.ai) - onchain analytics for crypto investors and AI agents.
14
14
 
15
+ ## CLI vs Direct API
16
+
17
+ If your agent has HTTP access (curl, fetch), you can skip the CLI and hit the [Nansen REST API](https://docs.nansen.ai) directly with `apiKey` header auth. The CLI is most useful for terminal-native agents (Claude Code, Codex, Cursor) that benefit from `--pretty`, `--table`, `--fields`, built-in retries, and schema introspection.
18
+
15
19
  ## Setup
16
20
 
17
21
  ```bash
18
22
  # Install globally
19
23
  npm install -g nansen-cli
20
24
 
21
- # Authenticate (interactive)
22
- nansen login
25
+ # Authenticate — pick the method that works for your context:
26
+
27
+ # Option A: Non-interactive (best for agents — no prompts, no wasted credits)
28
+ mkdir -p ~/.nansen && echo '{"apiKey":"YOUR_KEY","baseUrl":"https://api.nansen.ai"}' > ~/.nansen/config.json && chmod 600 ~/.nansen/config.json
23
29
 
24
- # Or set environment variable
30
+ # Option B: Environment variable (good for CI/scripts)
25
31
  export NANSEN_API_KEY=your-api-key
32
+
33
+ # Option C: Interactive login (burns 1 credit to validate)
34
+ nansen login
26
35
  ```
27
36
 
28
37
  Get your API key at [app.nansen.ai/api](https://app.nansen.ai/api).
29
38
 
39
+ ### Verify Installation
40
+
41
+ ```bash
42
+ # Free check (no API key needed):
43
+ nansen schema | head -1
44
+
45
+ # Full check (uses 1 credit):
46
+ nansen token screener --chain solana --limit 1
47
+ ```
48
+
30
49
  ## Commands
31
50
 
32
51
  ### Smart Money
@@ -82,7 +101,7 @@ nansen portfolio defi --wallet 0x123...
82
101
 
83
102
  ## Supported Chains
84
103
 
85
- ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, zksync, mantle, ronin, sei, sonic, monad, hyperevm
104
+ ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
86
105
 
87
106
  ## Smart Money Labels
88
107
 
@@ -96,24 +115,9 @@ nansen schema --pretty
96
115
  nansen schema smart-money --pretty
97
116
  ```
98
117
 
99
- ## Known Endpoint Issues
100
-
101
- ### Chain/Token-Specific Limitations
102
- - `token holders --smart-money` — Fails with `UNSUPPORTED_FILTER` for tokens without smart money tracking (e.g., WCT on Optimism). Not all tokens have smart money data. Do not retry.
103
- - `token flow-intelligence` — May return all-zero flows for tokens without significant smart money activity. This is normal, not an error.
104
-
105
- ### Credit Management
106
- - `profiler labels` and `profiler balance` consume credits. Budget ~20 calls per session.
107
- - `Insufficient credits` (403, code `CREDITS_EXHAUSTED`) is a hard stop — no retry will help.
108
- - Check your Nansen dashboard for credit balance: [app.nansen.ai](https://app.nansen.ai).
109
- - Run balance checks in batches of 3-4 to avoid burning credits on rate-limit retries.
118
+ ## Troubleshooting
110
119
 
111
- ### Error Codes to Watch
112
- | Code | Meaning | Action |
113
- |------|---------|--------|
114
- | `UNSUPPORTED_FILTER` | Filter not available for this token/chain | Remove the filter and retry, or skip this token |
115
- | `CREDITS_EXHAUSTED` | API credits depleted | Stop all API calls. Check dashboard. |
116
- | `RATE_LIMITED` | Too many requests (429) | Wait and retry (automatic with default retry) |
120
+ See [AGENTS.md Troubleshooting](AGENTS.md#troubleshooting) for the full troubleshooting guide, including error codes, known endpoint quirks, and pagination gotchas.
117
121
 
118
122
  ## Examples
119
123
 
package/TODO.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  > **Built by agents, for agents.** We prioritize improvements that create the best possible AI agent experience.
4
4
 
5
+ ## P0 - Trading Agent Support
6
+
7
+ ### Wallet Management
8
+ - [ ] `nansen wallet create` — generate a local wallet (per chain), store key securely on disk
9
+ - [ ] `nansen wallet address` — print the wallet address for funding
10
+ - [ ] `nansen wallet balance` — check wallet balance
11
+
12
+ ### Trading Execution
13
+ - [x] `nansen quote` — get a quote for a DEX swap (chain, tokens, amount)
14
+ - [x] `nansen execute` — sign and submit a trade via Nansen API (takes quote-id)
15
+ - [ ] ⚠️ EVM transaction signing — requires thorough security review before production use
16
+
17
+ > These commands are required for `nansen-trading-agent` — the autonomous trading sub-agent.
18
+ > Wallet commands should keep the agent self-contained with zero external dependencies.
19
+
20
+ ---
21
+
5
22
  ## P2 - Nice to Have
6
23
 
7
24
  ### Test Coverage Gaps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -9,10 +9,12 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node src/index.js",
12
+ "pretest": "node scripts/check-changeset.js",
12
13
  "test": "vitest run",
13
14
  "test:watch": "vitest",
14
15
  "test:coverage": "vitest run --coverage",
15
16
  "test:live": "NANSEN_LIVE_TEST=1 vitest run",
17
+ "test:swap": "vitest run --config vitest.e2e.config.js",
16
18
  "changeset": "changeset",
17
19
  "changeset:version": "changeset version",
18
20
  "changeset:publish": "changeset publish"
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Non-blocking check: warns if the current branch has no new changeset file
5
+ * compared to main. Runs as a pretest hook so agents and humans see a reminder.
6
+ * Always exits 0 — this is a nudge, not a gate.
7
+ */
8
+
9
+ import { execSync } from "child_process";
10
+
11
+ try {
12
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
13
+ if (branch === "main") process.exit(0);
14
+
15
+ const newChangesets = execSync(
16
+ "git diff main --name-only --diff-filter=A -- .changeset/*.md",
17
+ { encoding: "utf8" }
18
+ ).trim();
19
+
20
+ if (!newChangesets) {
21
+ console.error(
22
+ "\x1b[33m[changeset] No new changeset file found on this branch. " +
23
+ "If this PR changes user-facing behavior, add one: npx changeset\x1b[0m"
24
+ );
25
+ }
26
+ } catch {
27
+ // Not a git repo, main doesn't exist, etc. — skip silently.
28
+ }
package/src/api.js CHANGED
@@ -320,6 +320,11 @@ function loadConfig() {
320
320
  config = { apiKey: null, baseUrl: 'https://api.nansen.ai' };
321
321
  }
322
322
 
323
+ // Ensure baseUrl default (config file from older versions may omit it)
324
+ if (!config.baseUrl) {
325
+ config.baseUrl = 'https://api.nansen.ai';
326
+ }
327
+
323
328
  // Env vars override individual fields
324
329
  if (process.env.NANSEN_API_KEY) {
325
330
  config.apiKey = process.env.NANSEN_API_KEY;
@@ -386,6 +391,17 @@ function parseRetryAfter(headerValue) {
386
391
  return null;
387
392
  }
388
393
 
394
+ /**
395
+ * Build a date range from today back N days
396
+ * @param {number} days - Number of days back from today
397
+ * @returns {{from: string, to: string}} Date range with YYYY-MM-DD strings
398
+ */
399
+ export function buildDateRange(days) {
400
+ const to = new Date().toISOString().split('T')[0];
401
+ const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
402
+ return { from, to };
403
+ }
404
+
389
405
  export class NansenAPI {
390
406
  constructor(apiKey = config.apiKey, baseUrl = config.baseUrl, options = {}) {
391
407
  this.apiKey = apiKey || null;
@@ -489,7 +505,41 @@ export class NansenAPI {
489
505
  } else if (code === ErrorCode.CREDITS_EXHAUSTED) {
490
506
  message = message.replace(/\.+$/, '') + '. No retry will help. Check your Nansen dashboard for credit balance.';
491
507
  } else if (code === ErrorCode.PAYMENT_REQUIRED) {
492
- message = 'Payment required (x402). Sign the paymentRequirements below per https://docs.x402.org and pass the result with --x402-payment-signature <value>.';
508
+ // Try x402 auto-payment with fallback across payment networks
509
+ if (!this.defaultHeaders['Payment-Signature']) {
510
+ try {
511
+ const { createPaymentSignatures } = await import('./x402.js');
512
+ for await (const { signature, network } of createPaymentSignatures(response, url)) {
513
+ const paidResponse = await fetch(url, {
514
+ method: 'POST',
515
+ headers: {
516
+ 'Content-Type': 'application/json',
517
+ 'X-Client-Type': 'nansen-cli',
518
+ 'X-Client-Version': packageVersion,
519
+ 'Payment-Signature': signature,
520
+ ...this.defaultHeaders,
521
+ ...options.headers,
522
+ },
523
+ body: JSON.stringify(NansenAPI.cleanBody(body)),
524
+ });
525
+ if (paidResponse.ok) {
526
+ const chain = network.startsWith('solana:') ? 'Solana' : 'Base';
527
+ console.error(`[x402] Paid via ${chain} USDC`);
528
+ // Check remaining balance and warn if low
529
+ try {
530
+ const { checkX402Balance } = await import('./x402.js');
531
+ const balance = await checkX402Balance(network);
532
+ if (balance !== null && balance < 0.25) {
533
+ console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
534
+ }
535
+ } catch { /* balance check is best-effort */ }
536
+ return await paidResponse.json();
537
+ }
538
+ // This payment option was rejected, try next
539
+ }
540
+ } catch { /* x402 auto-pay unavailable, fall through */ }
541
+ }
542
+ message = 'Payment required. To access this endpoint:\n • Set an API key: nansen login --api-key <key> (get one at https://app.nansen.ai/api)\n • Or pay per call: nansen wallet create, fund with USDC on Base or Solana (from $0.01/call, min $0.05 balance)\n • Docs: https://docs.x402.org';
493
543
  const paymentHeader = response.headers.get('payment-required');
494
544
  if (paymentHeader) {
495
545
  try {
@@ -585,11 +635,9 @@ export class NansenAPI {
585
635
 
586
636
  async smartMoneyHistoricalHoldings(params = {}) {
587
637
  const { chains = ['solana'], filters = {}, orderBy, pagination, days = 30 } = params;
588
- const to = new Date().toISOString().split('T')[0];
589
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
590
638
  return this.request('/api/v1/smart-money/historical-holdings', {
591
639
  chains,
592
- date_range: { from, to },
640
+ date_range: buildDateRange(days),
593
641
  filters,
594
642
  order_by: orderBy,
595
643
  pagination
@@ -632,11 +680,7 @@ export class NansenAPI {
632
680
  const validation = validateAddress(address, chain);
633
681
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
634
682
  }
635
- const dateRange = date || (() => {
636
- const to = new Date().toISOString().split('T')[0];
637
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
638
- return { from, to };
639
- })();
683
+ const dateRange = date || buildDateRange(days);
640
684
  return this.request('/api/v1/profiler/address/transactions', {
641
685
  address,
642
686
  chain,
@@ -653,13 +697,7 @@ export class NansenAPI {
653
697
  const validation = validateAddress(address, chain);
654
698
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
655
699
  }
656
- // Build date range
657
- let dateRange = date;
658
- if (!dateRange) {
659
- const to = new Date().toISOString().split('T')[0];
660
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
661
- dateRange = { from, to };
662
- }
700
+ const dateRange = date || buildDateRange(days);
663
701
  return this.request('/api/v1/profiler/address/pnl', {
664
702
  address,
665
703
  chain,
@@ -695,12 +733,10 @@ export class NansenAPI {
695
733
  const validation = validateAddress(address, chain);
696
734
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
697
735
  }
698
- const to = new Date().toISOString().split('T')[0];
699
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
700
736
  return this.request('/api/v1/profiler/address/historical-balances', {
701
737
  address,
702
738
  chain,
703
- date: { from, to },
739
+ date: buildDateRange(days),
704
740
  filters,
705
741
  order_by: orderBy,
706
742
  pagination
@@ -727,12 +763,10 @@ export class NansenAPI {
727
763
  const validation = validateAddress(address, chain);
728
764
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
729
765
  }
730
- const to = new Date().toISOString().split('T')[0];
731
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
732
766
  return this.request('/api/v1/profiler/address/counterparties', {
733
767
  address,
734
768
  chain,
735
- date: { from, to },
769
+ date: buildDateRange(days),
736
770
  filters,
737
771
  order_by: orderBy,
738
772
  pagination
@@ -745,12 +779,10 @@ export class NansenAPI {
745
779
  const validation = validateAddress(address, chain);
746
780
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
747
781
  }
748
- const to = new Date().toISOString().split('T')[0];
749
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
750
782
  return this.request('/api/v1/profiler/address/pnl-summary', {
751
783
  address,
752
784
  chain,
753
- date: { from, to },
785
+ date: buildDateRange(days),
754
786
  order_by: orderBy,
755
787
  pagination
756
788
  });
@@ -769,11 +801,9 @@ export class NansenAPI {
769
801
 
770
802
  async addressPerpTrades(params = {}) {
771
803
  const { address, filters = {}, orderBy, pagination, days = 30 } = params;
772
- const to = new Date().toISOString().split('T')[0];
773
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
774
804
  return this.request('/api/v1/profiler/perp-trades', {
775
805
  address,
776
- date: { from, to },
806
+ date: buildDateRange(days),
777
807
  filters,
778
808
  order_by: orderBy,
779
809
  pagination
@@ -815,11 +845,7 @@ export class NansenAPI {
815
845
  const validation = validateTokenAddress(tokenAddress, chain);
816
846
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
817
847
  }
818
- const dateRange = date || (() => {
819
- const to = new Date().toISOString().split('T')[0];
820
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
821
- return { from, to };
822
- })();
848
+ const dateRange = date || buildDateRange(days);
823
849
  return this.request('/api/v1/tgm/flows', {
824
850
  token_address: tokenAddress,
825
851
  chain,
@@ -836,9 +862,6 @@ export class NansenAPI {
836
862
  const validation = validateTokenAddress(tokenAddress, chain);
837
863
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
838
864
  }
839
- const to = new Date().toISOString().split('T')[0];
840
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
841
-
842
865
  // Apply smart money filter via filters object
843
866
  if (onlySmartMoney) {
844
867
  filters.include_smart_money_labels = filters.include_smart_money_labels ||
@@ -848,7 +871,7 @@ export class NansenAPI {
848
871
  return this.request('/api/v1/tgm/dex-trades', {
849
872
  token_address: tokenAddress,
850
873
  chain,
851
- date: { from, to },
874
+ date: buildDateRange(days),
852
875
  filters,
853
876
  order_by: orderBy,
854
877
  pagination
@@ -861,12 +884,10 @@ export class NansenAPI {
861
884
  const validation = validateTokenAddress(tokenAddress, chain);
862
885
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
863
886
  }
864
- const to = new Date().toISOString().split('T')[0];
865
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
866
887
  return this.request('/api/v1/tgm/pnl-leaderboard', {
867
888
  token_address: tokenAddress,
868
889
  chain,
869
- date: { from, to },
890
+ date: buildDateRange(days),
870
891
  filters,
871
892
  order_by: orderBy,
872
893
  pagination
@@ -879,11 +900,7 @@ export class NansenAPI {
879
900
  const validation = validateTokenAddress(tokenAddress, chain);
880
901
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
881
902
  }
882
- const dateRange = date || (() => {
883
- const to = new Date().toISOString().split('T')[0];
884
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
885
- return { from, to };
886
- })();
903
+ const dateRange = date || buildDateRange(days);
887
904
  return this.request('/api/v1/tgm/who-bought-sold', {
888
905
  token_address: tokenAddress,
889
906
  chain,
@@ -912,12 +929,10 @@ export class NansenAPI {
912
929
  const validation = validateTokenAddress(tokenAddress, chain);
913
930
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
914
931
  }
915
- const to = new Date().toISOString().split('T')[0];
916
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
917
932
  return this.request('/api/v1/tgm/transfers', {
918
933
  token_address: tokenAddress,
919
934
  chain,
920
- date: { from, to },
935
+ date: buildDateRange(days),
921
936
  filters,
922
937
  order_by: orderBy,
923
938
  pagination
@@ -941,11 +956,9 @@ export class NansenAPI {
941
956
 
942
957
  async tokenPerpTrades(params = {}) {
943
958
  const { tokenSymbol, filters = {}, orderBy, pagination, days = 30 } = params;
944
- const to = new Date().toISOString().split('T')[0];
945
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
946
959
  return this.request('/api/v1/tgm/perp-trades', {
947
960
  token_symbol: tokenSymbol,
948
- date: { from, to },
961
+ date: buildDateRange(days),
949
962
  filters,
950
963
  order_by: orderBy,
951
964
  pagination
@@ -964,11 +977,9 @@ export class NansenAPI {
964
977
 
965
978
  async tokenPerpPnlLeaderboard(params = {}) {
966
979
  const { tokenSymbol, filters = {}, orderBy, pagination, days = 30 } = params;
967
- const to = new Date().toISOString().split('T')[0];
968
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
969
980
  return this.request('/api/v1/tgm/perp-pnl-leaderboard', {
970
981
  token_symbol: tokenSymbol,
971
- date: { from, to },
982
+ date: buildDateRange(days),
972
983
  filters,
973
984
  order_by: orderBy,
974
985
  pagination
@@ -987,6 +998,20 @@ export class NansenAPI {
987
998
  });
988
999
  }
989
1000
 
1001
+ async tokenOhlcv(params = {}) {
1002
+ const { tokenAddress, chain = 'solana', timeframe, pagination } = params;
1003
+ if (tokenAddress) {
1004
+ const validation = validateTokenAddress(tokenAddress, chain);
1005
+ if (!validation.valid) throw new NansenError(validation.error, validation.code);
1006
+ }
1007
+ return this.request('/api/v1/tgm/token-ohlcv', {
1008
+ token_address: tokenAddress,
1009
+ chain,
1010
+ timeframe,
1011
+ pagination
1012
+ });
1013
+ }
1014
+
990
1015
  async tokenInformation(params = {}) {
991
1016
  const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
992
1017
  if (tokenAddress) {
@@ -1004,10 +1029,8 @@ export class NansenAPI {
1004
1029
 
1005
1030
  async perpScreener(params = {}) {
1006
1031
  const { filters = {}, orderBy, pagination, days = 30 } = params;
1007
- const to = new Date().toISOString().split('T')[0];
1008
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
1009
1032
  return this.request('/api/v1/perp-screener', {
1010
- date: { from, to },
1033
+ date: buildDateRange(days),
1011
1034
  filters,
1012
1035
  order_by: orderBy,
1013
1036
  pagination
@@ -1016,10 +1039,8 @@ export class NansenAPI {
1016
1039
 
1017
1040
  async perpLeaderboard(params = {}) {
1018
1041
  const { filters = {}, orderBy, pagination, days = 30 } = params;
1019
- const to = new Date().toISOString().split('T')[0];
1020
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
1021
1042
  return this.request('/api/v1/perp-leaderboard', {
1022
- date: { from, to },
1043
+ date: buildDateRange(days),
1023
1044
  filters,
1024
1045
  order_by: orderBy,
1025
1046
  pagination