nansen-cli 1.30.0 → 1.30.2

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.30.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#431](https://github.com/nansen-ai/nansen-cli/pull/431) [`c2c033b`](https://github.com/nansen-ai/nansen-cli/commit/c2c033b86f9dab59df9497ce971ef6f267ee3669) Thanks [@MarcLlopart](https://github.com/MarcLlopart)! - Pass backend quoteId in execute requests for BI correlation
8
+
9
+ ## 1.30.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [#411](https://github.com/nansen-ai/nansen-cli/pull/411) [`26cd863`](https://github.com/nansen-ai/nansen-cli/commit/26cd863de0604f2d50750ff6742c46772f0b661e) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen-limit-orders` skill. The skill teaches agents to use the native `nansen trade limit-order create|list|cancel|update` commands for Solana price-triggered orders, and documents the alert-based settlement-signal fallback (`common-token-transfer` smart alert on the settlement wallet) for chains without native limit-order support. Builds on the `trade limit-order` command surface added by #328.
14
+
15
+ - [#429](https://github.com/nansen-ai/nansen-cli/pull/429) [`511e795`](https://github.com/nansen-ai/nansen-cli/commit/511e7959d735d38e1ee44d2aa29ce19df55b9336) Thanks [@gulshngill](https://github.com/gulshngill)! - Improve discovery of `nansen trade` in package metadata, help output, install tips, and agent-facing docs.
16
+
3
17
  ## 1.30.0
4
18
 
5
19
  ### Minor Changes
package/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  [![npm version](https://img.shields.io/npm/v/nansen-cli.svg)](https://www.npmjs.com/package/nansen-cli)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- > **Built by agents, for agents.** Command-line interface for the [Nansen API](https://docs.nansen.ai), designed for AI agents.
6
+ > **Built by agents, for agents.** Command-line interface for the [Nansen API](https://docs.nansen.ai), designed for AI agents to research on-chain data, manage wallets, and trade through `nansen trade`.
7
+
8
+ Use it for both analytics and execution: `nansen research ...` returns structured on-chain data, while `nansen trade quote` / `nansen trade execute` handle DEX swaps on Solana and Base, including cross-chain bridges.
7
9
 
8
10
  ## Installation
9
11
 
@@ -35,14 +37,15 @@ Three options — pick whichever fits your setup:
35
37
  nansen research <category> <subcommand> [options]
36
38
  nansen agent "<question>" # AI research agent (200 credits, Pro)
37
39
  nansen agent "<question>" --expert # deeper analysis (750 credits, Pro)
38
- nansen trade <subcommand> [options]
40
+ nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
41
+ nansen trade execute --quote <quoteId>
39
42
  nansen wallet <subcommand> [options]
40
43
  nansen schema [command] [--pretty] # full command reference (no API key needed)
41
44
  ```
42
45
 
43
46
  **Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `prediction-market` (`pm`), `search`, `perp`, `points`
44
47
 
45
- **Trade:** `quote`, `execute`, `bridge-status` — DEX swaps on Solana and Base, including cross-chain bridges.
48
+ **Trade:** `quote`, `execute`, `bridge-status`, `limit-order` — DEX swaps on Solana and Base, cross-chain bridges, and Solana limit orders.
46
49
 
47
50
  **Wallet:** `create`, `list`, `show`, `export`, `default`, `delete`, `send` — local or Privy server-side wallets (EVM + Solana).
48
51
 
@@ -68,6 +71,28 @@ nansen trade bridge-status --tx-hash <hash> --from-chain base --to-chain solana
68
71
 
69
72
  Amounts are in base units (lamports, wei) by default — use `--amount-unit token|usd|percent` for friendlier inputs. Common symbols (`SOL`, `ETH`, `USDC`, `USDT`) resolve automatically. A wallet is required — set one with `nansen wallet default <name>`.
70
73
 
74
+ ## Limit Orders
75
+
76
+ Native price-triggered orders on **Solana**. Four subcommands:
77
+
78
+ ```bash
79
+ nansen trade limit-order create \
80
+ --from SOL --to USDC \
81
+ --amount 1.5 \
82
+ --trigger-mint SOL --trigger-condition below --trigger-price 80 \
83
+ --slippage-bps 300 --expires 7d
84
+
85
+ nansen trade limit-order list # all orders
86
+ nansen trade limit-order list --state active # only open
87
+ nansen trade limit-order list --state past # filled or cancelled
88
+ nansen trade limit-order cancel --order <orderId>
89
+ nansen trade limit-order update --order <orderId> --trigger-price 85
90
+ ```
91
+
92
+ `--amount` is in token units (`1.5` = 1.5 SOL). `--slippage-bps` is basis points (`300` = 3%, `100` = 1%); omit for auto. Minimum order value ~$10 (server-enforced). Local, Privy, and WalletConnect wallets all work.
93
+
94
+ For EVM chains, there's no native limit-order surface — pair an external venue's resting order with a `common-token-transfer` smart alert on the settlement wallet as a best-effort fill signal. See the `nansen-limit-orders` skill for details.
95
+
71
96
  ## Wallet
72
97
 
73
98
  ```bash
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.30.0",
4
- "description": "Command-line interface for Nansen API - designed for AI agents",
3
+ "version": "1.30.2",
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",
7
7
  "bin": {
@@ -42,6 +42,9 @@
42
42
  "smart-money",
43
43
  "onchain",
44
44
  "defi",
45
+ "trading",
46
+ "dex",
47
+ "swaps",
45
48
  "solana",
46
49
  "ethereum"
47
50
  ],
@@ -104,7 +104,7 @@ async function installSkill() {
104
104
  }
105
105
 
106
106
  log(`The Nansen skill lets AI coding agents (Cursor, Claude Code, etc.) query`);
107
- log(`on-chain data, track smart money, and analyze tokens on your behalf.`);
107
+ log(`on-chain data, track smart money, analyze tokens, and use nansen trade.`);
108
108
  const answer = await prompt(` Install Nansen skill for your AI coding agent? [Y/n] `);
109
109
 
110
110
  if (/^n/i.test(answer)) {
@@ -133,6 +133,7 @@ async function testQuery() {
133
133
 
134
134
  if (/^n/i.test(answer)) {
135
135
  log(`Skipped. You're all set! Try: ${CYAN}nansen research smart-money netflow --chain solana${RESET}`);
136
+ log(`For trading: ${CYAN}nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000${RESET}`);
136
137
  return;
137
138
  }
138
139
 
@@ -162,6 +163,7 @@ async function main() {
162
163
  log();
163
164
  log(`Tip: Run '${CYAN}npx skills add ${SKILL_REPO}${RESET}' to install the Nansen AI coding skill.`);
164
165
  log(`Tip: Run '${CYAN}nansen login --api-key <key>${RESET}' to authenticate.`);
166
+ log(`Tip: To trade, first create a wallet with '${CYAN}nansen wallet create${RESET}', then quote with '${CYAN}nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000${RESET}' and execute with '${CYAN}nansen trade execute --quote <id>${RESET}'.`);
165
167
  return;
166
168
  }
167
169
 
@@ -0,0 +1,236 @@
1
+ ---
2
+ name: nansen-limit-orders
3
+ description: Guide users through native limit orders on Solana via `nansen trade limit-order create|list|cancel|update`, and the alert-based settlement-signal fallback for chains without native support. Use when a user wants a price-triggered buy or sell.
4
+ metadata:
5
+ openclaw:
6
+ requires:
7
+ env:
8
+ - NANSEN_API_KEY
9
+ bins:
10
+ - nansen
11
+ primaryEnv: NANSEN_API_KEY
12
+ install:
13
+ - kind: node
14
+ package: nansen-cli
15
+ bins: [nansen]
16
+ allowed-tools: Bash(nansen:*)
17
+ ---
18
+
19
+ # Limit Orders
20
+
21
+ Use this skill when the user wants a price-triggered order. There are two
22
+ distinct paths — pick the one that matches the user's chain:
23
+
24
+ - **Solana → native limit orders.** `nansen trade limit-order create|list|cancel|update`
25
+ places real resting orders through the Nansen trading API. Use these for
26
+ anything on Solana.
27
+ - **Other chains → alert-based approximation.** `nansen-cli` does not yet place
28
+ native limit orders on EVM chains. Place the resting order on the venue that
29
+ supports it (CEX, DEX limit-order product) and create a companion
30
+ `common-token-transfer` smart alert on the settlement wallet as a best-effort
31
+ fill signal.
32
+
33
+ ## Prerequisites
34
+
35
+ - A Solana wallet configured in `nansen-cli`: `nansen wallet show <name>` (or
36
+ `nansen wallet create` if none exists). Local, Privy, and WalletConnect
37
+ wallets are all supported for `trade limit-order`.
38
+ - The wallet must hold the sell token plus a small amount of SOL for fees.
39
+ - For the alert fallback: a notification channel (Telegram chat ID, Slack or
40
+ Discord webhook, or generic webhook URL).
41
+ - `NANSEN_API_KEY`. Smart alerts are internal-only; non-internal users get 404.
42
+ - First-time `trade limit-order create` auto-registers a trading vault and
43
+ caches a JWT at `~/.nansen/limit-order-auth.json` for ~23h.
44
+
45
+ > **Two mechanisms, not one.** The limit order itself is **price-triggered** —
46
+ > it executes when the market price crosses the target. A companion smart
47
+ > alert is a **settlement confirmation** — it fires after the trade settles
48
+ > on-chain (i.e. when the bought token actually arrives in the wallet). They
49
+ > are independent: the order handles the trigger, the alert tells the user the
50
+ > fill landed.
51
+
52
+ ## Solana: Native Limit Orders
53
+
54
+ ### Create
55
+
56
+ ```bash
57
+ # --amount is always in token units (e.g. 1 = 1 SOL, 80 = 80 USDC)
58
+ nansen trade limit-order create \
59
+ --from SOL --to USDC \
60
+ --amount 1 \
61
+ --trigger-mint SOL --trigger-condition below --trigger-price 80
62
+
63
+ # with explicit slippage and expiry
64
+ nansen trade limit-order create \
65
+ --from SOL --to USDC \
66
+ --amount 1.5 \
67
+ --trigger-mint SOL --trigger-condition above --trigger-price 200 \
68
+ --slippage-bps 300 \
69
+ --expires 7d
70
+ ```
71
+
72
+ Required flags: `--from`, `--to`, `--amount`, `--trigger-mint`,
73
+ `--trigger-condition` (`above` or `below`), `--trigger-price` (USD).
74
+
75
+ Key options:
76
+
77
+ - `--amount` is always in **token units** (human-readable). `1.5` means 1.5 SOL,
78
+ `80` means 80 USDC. There is no base-unit mode and no amount-unit override flag —
79
+ do not pass one.
80
+ - `--slippage-bps <bps>` — basis points, integer 0–10000 (`300` = 3%, `100` = 1%).
81
+ Omit for auto.
82
+ - `--expires` accepts `24h`, `7d`, `30d` (default), or an epoch-ms timestamp.
83
+ - `--wallet <name>` or `--wallet walletconnect` (alias `wc`) to pick a non-default
84
+ wallet.
85
+
86
+ Constraints (all enforced server-side — surface errors to the user as-is):
87
+
88
+ - Minimum order value (~$10 last seen). Below this the API rejects the order.
89
+ - `--from` and `--to` must be valid Solana mint addresses or supported symbols
90
+ (SOL, USDC, USDT, etc.). Resolve unknown tokens with `nansen research search`.
91
+ - Tokens with transfer-hook extensions (e.g. some pump.fun tokens) are rejected
92
+ at create time.
93
+
94
+ ### List
95
+
96
+ ```bash
97
+ nansen trade limit-order list # default
98
+ nansen trade limit-order list --state active # only open
99
+ nansen trade limit-order list --state past # filled or cancelled
100
+ nansen trade limit-order list --mint <mintAddr> # filter by token
101
+ nansen trade limit-order list --limit 50 --offset 0 --dir desc
102
+ ```
103
+
104
+ Options: `--state active|past`, `--mint <addr>`, `--limit <n>` (default 20),
105
+ `--offset <n>`, `--sort <field>`, `--dir asc|desc` (default `desc`),
106
+ `--wallet <name>`.
107
+
108
+ ### Cancel
109
+
110
+ ```bash
111
+ nansen trade limit-order cancel --order <orderId>
112
+ ```
113
+
114
+ Cancelling submits a withdrawal transaction; surface the tx signature from the
115
+ CLI output so the user can verify on Solscan.
116
+
117
+ ### Update
118
+
119
+ ```bash
120
+ nansen trade limit-order update --order <orderId> --trigger-price 85
121
+ nansen trade limit-order update --order <orderId> --slippage-bps 100
122
+ ```
123
+
124
+ Only `--trigger-price` and `--slippage-bps` can be updated. To change size or
125
+ the token pair, cancel and re-create. Auto slippage can only be set at creation
126
+ time (by omitting `--slippage-bps` from `create`); `update` cannot revert a
127
+ fixed slippage back to auto.
128
+
129
+ ## Non-Solana Chains: Alert-Based Settlement Signal
130
+
131
+ `nansen-cli` does not currently place native limit orders on EVM chains. The
132
+ supported approximation is:
133
+
134
+ 1. Place the resting limit order on the venue or product that supports it (CEX,
135
+ DEX limit-order product, aggregator). **The venue handles the price
136
+ trigger.**
137
+ 2. Use the same wallet as the settlement wallet.
138
+ 3. Create a `common-token-transfer` smart alert scoped to wallet + chain +
139
+ token + side. The alert fires **after** the trade settles — i.e. once the
140
+ bought/sold token actually moves on-chain — as a fill-detected signal.
141
+
142
+ How the two mechanisms compose:
143
+
144
+ 1. **Price hits target → the venue's limit order executes.** This is the
145
+ trigger. `nansen-cli` is not involved.
146
+ 2. **Token arrives in the wallet → the smart alert pings.** This is the
147
+ settlement confirmation. The alert never sees the price trigger; it only
148
+ sees the resulting transfer.
149
+
150
+ This is a best-effort settlement signal, not authoritative order tracking. It
151
+ does **not** expose order-state polling, partial-fill progress, `triggeredAt`,
152
+ `fillPercent`, remaining size, or canonical filled/cancelled history.
153
+
154
+ ### Buy-Side Settlement Alert
155
+
156
+ ```bash
157
+ nansen alerts create \
158
+ --name 'Settlement signal: buy PEPE on trading wallet' \
159
+ --type common-token-transfer \
160
+ --chains ethereum \
161
+ --events buy \
162
+ --subject address:0xYourWallet \
163
+ --token 0x6982508145454ce325ddbe47a25d4ec3d2311933:ethereum \
164
+ --telegram 5238612255
165
+ ```
166
+
167
+ ### Sell-Side Settlement Alert
168
+
169
+ ```bash
170
+ nansen alerts create \
171
+ --name 'Settlement signal: sell USDC on trading wallet' \
172
+ --type common-token-transfer \
173
+ --chains base \
174
+ --events sell \
175
+ --subject address:0xYourWallet \
176
+ --token 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913:base \
177
+ --telegram 5238612255
178
+ ```
179
+
180
+ ### Alert Hardening
181
+
182
+ - `--usd-min <amount>` to suppress dust fills.
183
+ - `--description '<limit price / venue / notes>'` so the alert records intent.
184
+ - Do **not** recommend a wallet-wide transfer alert with no token filter — it
185
+ overfires.
186
+ - Do **not** describe alert delivery as "order filled" or "triggered". The
187
+ alert is only evidence that a matching token transfer was observed on the
188
+ wallet — not precise fill detection.
189
+ - If the venue settles in a way the alerting backend classifies as a generic
190
+ transfer rather than `buy`/`sell`, a narrow alert may miss it. Only widen to
191
+ `--events buy,receive` or `--events sell,send` if the user accepts broader
192
+ matching and the risk of unrelated matches.
193
+
194
+ ## Optional: Belt-and-Braces on Solana
195
+
196
+ For Solana native limit orders, a companion `common-token-transfer` alert on
197
+ the settlement wallet is optional but useful. **It is a settlement
198
+ confirmation, not a price trigger.** The limit order itself handles the price
199
+ trigger; the alert fires when the bought token actually arrives in the wallet
200
+ (or, for sells, when the sold token leaves) — proof the on-chain fill landed,
201
+ delivered via Telegram/webhook independently of `trade limit-order list`
202
+ polling.
203
+
204
+ Some venues route fills through programs that the alerting backend may not
205
+ classify as `buy`/`sell`, so this is still a best-effort settlement signal,
206
+ not authoritative fill detection. Pair with the same `common-token-transfer`
207
+ alert shape shown above.
208
+
209
+ ## What to Tell the User
210
+
211
+ When suggesting a companion alert, be explicit that two different mechanisms
212
+ are at work — users routinely conflate them:
213
+
214
+ - The **limit order** is the price trigger. It executes when the market price
215
+ crosses the target.
216
+ - The **smart alert** is the settlement confirmation. It pings after the trade
217
+ settles on-chain.
218
+
219
+ Suggested phrasing: _"When your order fills and the token arrives in your
220
+ wallet, you'll get a Telegram ping. The price trigger is handled by the limit
221
+ order itself — the alert just confirms the fill landed."_
222
+
223
+ Avoid wording that implies the alert "triggers" the order or that the alert
224
+ itself watches price. It does neither.
225
+
226
+ ## Notes
227
+
228
+ - Chain aliases for alerts: Hyperliquid = `hyperevm`, BSC = `bnb`.
229
+ - Use single quotes for names with `$` or special characters.
230
+ - For immediate swaps (not price-triggered), use the `nansen-trading` skill.
231
+ - For webhook delivery of alerts, pair with `nansen-alerts-webhook-listener`.
232
+
233
+ ## Source
234
+
235
+ - npm: https://www.npmjs.com/package/nansen-cli
236
+ - GitHub: https://github.com/nansen-ai/nansen-cli
@@ -19,6 +19,10 @@ allowed-tools: Bash(nansen:*)
19
19
 
20
20
  # Trade
21
21
 
22
+ Use the built-in `nansen trade` command for user requests to buy, sell, swap, bridge, or create Solana limit orders. Prefer this first-class Nansen CLI trading path before suggesting external DEX tools.
23
+
24
+ Subcommands: `quote`, `execute`, `bridge-status`, `limit-order`.
25
+
22
26
  Two-step flow: quote then execute. **Trades are irreversible once on-chain.**
23
27
 
24
28
  **Prerequisite:** You need a wallet first. Run `nansen wallet create` before trading.
@@ -77,6 +81,27 @@ After executing a cross-chain swap, the CLI polls bridge status automatically. T
77
81
  nansen trade bridge-status --tx-hash <hash> --from-chain base --to-chain solana
78
82
  ```
79
83
 
84
+ ## Limit Orders
85
+
86
+ Create and manage Solana limit orders:
87
+
88
+ ```bash
89
+ nansen trade limit-order create \
90
+ --from SOL \
91
+ --to USDC \
92
+ --amount 1.5 \
93
+ --trigger-mint SOL \
94
+ --trigger-condition below \
95
+ --trigger-price 80 \
96
+ --slippage-bps 300
97
+
98
+ nansen trade limit-order list
99
+ nansen trade limit-order cancel --order <order-id>
100
+ nansen trade limit-order update --order <order-id> --trigger-price 85
101
+ ```
102
+
103
+ `--slippage-bps` is basis points (`300` = 3%, `100` = 1%); omit for auto.
104
+
80
105
  ## Agent pattern
81
106
 
82
107
  ```bash
package/src/api.js CHANGED
@@ -336,6 +336,22 @@ export function validateTokenAddress(tokenAddress, chain = 'solana') {
336
336
  return validateAddress(tokenAddress, chain);
337
337
  }
338
338
 
339
+ /**
340
+ * Throw if address is present but invalid.
341
+ */
342
+ function requireValidAddress(address, chain) {
343
+ const v = validateAddress(address, chain);
344
+ if (!v.valid) throw new NansenError(v.error, v.code);
345
+ }
346
+
347
+ /**
348
+ * Throw if token address is present but invalid.
349
+ */
350
+ function requireValidToken(tokenAddress, chain) {
351
+ const v = validateTokenAddress(tokenAddress, chain);
352
+ if (!v.valid) throw new NansenError(v.error, v.code);
353
+ }
354
+
339
355
  function loadConfig() {
340
356
  // Base config from files, then env vars override individual fields
341
357
  let config = null;
@@ -791,10 +807,7 @@ export class NansenAPI {
791
807
 
792
808
  async addressBalance(params = {}) {
793
809
  const { address, entityName, chain = 'all', hideSpamToken = true, filters = {}, orderBy } = params;
794
- if (address) {
795
- const validation = validateAddress(address, chain);
796
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
797
- }
810
+ if (address) requireValidAddress(address, chain);
798
811
  return this.request('/api/v1/profiler/address/current-balance', {
799
812
  address,
800
813
  entity_name: entityName,
@@ -807,10 +820,7 @@ export class NansenAPI {
807
820
 
808
821
  async addressLabels(params = {}) {
809
822
  const { address, chain = 'ethereum', pagination = { page: 1, per_page: 100 } } = params;
810
- if (address) {
811
- const validation = validateAddress(address, chain);
812
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
813
- }
823
+ if (address) requireValidAddress(address, chain);
814
824
  return this.request('/api/beta/profiler/address/labels', {
815
825
  parameters: { address, chain },
816
826
  pagination
@@ -819,10 +829,7 @@ export class NansenAPI {
819
829
 
820
830
  async addressTransactions(params = {}) {
821
831
  const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30, date } = params;
822
- if (address) {
823
- const validation = validateAddress(address, chain);
824
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
825
- }
832
+ if (address) requireValidAddress(address, chain);
826
833
  const dateRange = date || buildDateRange(days);
827
834
  return this.request('/api/v1/profiler/address/transactions', {
828
835
  address,
@@ -836,10 +843,7 @@ export class NansenAPI {
836
843
 
837
844
  async addressPnl(params = {}) {
838
845
  const { address, chain = 'ethereum', date, days = 30, filters = {}, orderBy, pagination } = params;
839
- if (address) {
840
- const validation = validateAddress(address, chain);
841
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
842
- }
846
+ if (address) requireValidAddress(address, chain);
843
847
  const dateRange = date || buildDateRange(days);
844
848
  return this.request('/api/v1/profiler/address/pnl', {
845
849
  address,
@@ -899,10 +903,7 @@ export class NansenAPI {
899
903
 
900
904
  async addressHistoricalBalances(params = {}) {
901
905
  const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
902
- if (address) {
903
- const validation = validateAddress(address, chain);
904
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
905
- }
906
+ if (address) requireValidAddress(address, chain);
906
907
  return this.request('/api/v1/profiler/address/historical-balances', {
907
908
  address,
908
909
  chain,
@@ -915,10 +916,7 @@ export class NansenAPI {
915
916
 
916
917
  async addressRelatedWallets(params = {}) {
917
918
  const { address, chain = 'ethereum', orderBy, pagination } = params;
918
- if (address) {
919
- const validation = validateAddress(address, chain);
920
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
921
- }
919
+ if (address) requireValidAddress(address, chain);
922
920
  return this.request('/api/v1/profiler/address/related-wallets', {
923
921
  address,
924
922
  chain,
@@ -929,10 +927,7 @@ export class NansenAPI {
929
927
 
930
928
  async addressCounterparties(params = {}) {
931
929
  const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
932
- if (address) {
933
- const validation = validateAddress(address, chain);
934
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
935
- }
930
+ if (address) requireValidAddress(address, chain);
936
931
  return this.request('/api/v1/profiler/address/counterparties', {
937
932
  address,
938
933
  chain,
@@ -947,10 +942,7 @@ export class NansenAPI {
947
942
  // Note: pnl-summary endpoint is non-paginated (returns aggregate stats, not a list).
948
943
  // Pagination param intentionally omitted from this request.
949
944
  const { address, chain = 'ethereum', orderBy, days = 30 } = params;
950
- if (address) {
951
- const validation = validateAddress(address, chain);
952
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
953
- }
945
+ if (address) requireValidAddress(address, chain);
954
946
  return this.request('/api/v1/profiler/address/pnl-summary', {
955
947
  address,
956
948
  chain,
@@ -996,10 +988,7 @@ export class NansenAPI {
996
988
 
997
989
  async tokenHolders(params = {}) {
998
990
  const { tokenAddress, chain = 'solana', labelType = 'all_holders', filters = {}, orderBy, pagination, withLabels } = params;
999
- if (tokenAddress) {
1000
- const validation = validateTokenAddress(tokenAddress, chain);
1001
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1002
- }
991
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1003
992
  const body = {
1004
993
  token_address: tokenAddress,
1005
994
  chain,
@@ -1014,10 +1003,7 @@ export class NansenAPI {
1014
1003
 
1015
1004
  async tokenFlows(params = {}) {
1016
1005
  const { tokenAddress, chain = 'solana', label, filters = {}, orderBy, pagination, days = 30, date } = params;
1017
- if (tokenAddress) {
1018
- const validation = validateTokenAddress(tokenAddress, chain);
1019
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1020
- }
1006
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1021
1007
  const dateRange = date || buildDateRange(days);
1022
1008
  return this.request('/api/v1/tgm/flows', {
1023
1009
  token_address: tokenAddress,
@@ -1032,10 +1018,7 @@ export class NansenAPI {
1032
1018
 
1033
1019
  async tokenDexTrades(params = {}) {
1034
1020
  const { tokenAddress, chain = 'solana', onlySmartMoney = false, filters = {}, orderBy, pagination, days = 7 } = params;
1035
- if (tokenAddress) {
1036
- const validation = validateTokenAddress(tokenAddress, chain);
1037
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1038
- }
1021
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1039
1022
  // Apply smart money filter via filters object
1040
1023
  if (onlySmartMoney) {
1041
1024
  filters.include_smart_money_labels = filters.include_smart_money_labels ||
@@ -1054,10 +1037,7 @@ export class NansenAPI {
1054
1037
 
1055
1038
  async tokenPnlLeaderboard(params = {}) {
1056
1039
  const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30, withLabels } = params;
1057
- if (tokenAddress) {
1058
- const validation = validateTokenAddress(tokenAddress, chain);
1059
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1060
- }
1040
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1061
1041
  const body = {
1062
1042
  token_address: tokenAddress,
1063
1043
  chain,
@@ -1072,10 +1052,7 @@ export class NansenAPI {
1072
1052
 
1073
1053
  async tokenWhoBoughtSold(params = {}) {
1074
1054
  const { tokenAddress, chain = 'solana', buyOrSell = 'BUY', filters = {}, orderBy, pagination, days = 30, date } = params;
1075
- if (tokenAddress) {
1076
- const validation = validateTokenAddress(tokenAddress, chain);
1077
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1078
- }
1055
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1079
1056
  const dateRange = date || buildDateRange(days);
1080
1057
  return this.request('/api/v1/tgm/who-bought-sold', {
1081
1058
  token_address: tokenAddress,
@@ -1090,10 +1067,7 @@ export class NansenAPI {
1090
1067
 
1091
1068
  async tokenFlowIntelligence(params = {}) {
1092
1069
  const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
1093
- if (tokenAddress) {
1094
- const validation = validateTokenAddress(tokenAddress, chain);
1095
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1096
- }
1070
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1097
1071
  return this.request('/api/v1/tgm/flow-intelligence', {
1098
1072
  token_address: tokenAddress,
1099
1073
  chain,
@@ -1103,10 +1077,7 @@ export class NansenAPI {
1103
1077
 
1104
1078
  async tokenTransfers(params = {}) {
1105
1079
  const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 7 } = params;
1106
- if (tokenAddress) {
1107
- const validation = validateTokenAddress(tokenAddress, chain);
1108
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1109
- }
1080
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1110
1081
  return this.request('/api/v1/tgm/transfers', {
1111
1082
  token_address: tokenAddress,
1112
1083
  chain,
@@ -1120,10 +1091,7 @@ export class NansenAPI {
1120
1091
  async tokenJupDca(params = {}) {
1121
1092
  const { tokenAddress, filters = {}, orderBy, pagination } = params;
1122
1093
  // JUP DCA is Solana-only
1123
- if (tokenAddress) {
1124
- const validation = validateTokenAddress(tokenAddress, 'solana');
1125
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1126
- }
1094
+ if (tokenAddress) requireValidToken(tokenAddress, 'solana');
1127
1095
  return this.request('/api/v1/tgm/jup-dca', {
1128
1096
  token_address: tokenAddress,
1129
1097
  filters,
@@ -1168,10 +1136,7 @@ export class NansenAPI {
1168
1136
 
1169
1137
  async tokenIndicators(params = {}) {
1170
1138
  const { tokenAddress, chain = 'ethereum' } = params;
1171
- if (tokenAddress) {
1172
- const validation = validateTokenAddress(tokenAddress, chain);
1173
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1174
- }
1139
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1175
1140
  return this.request('/api/v1/tgm/indicators', {
1176
1141
  token_address: tokenAddress,
1177
1142
  chain
@@ -1180,10 +1145,7 @@ export class NansenAPI {
1180
1145
 
1181
1146
  async tokenOhlcv(params = {}) {
1182
1147
  const { tokenAddress, chain = 'solana', timeframe } = params;
1183
- if (tokenAddress) {
1184
- const validation = validateTokenAddress(tokenAddress, chain);
1185
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1186
- }
1148
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1187
1149
  return this.request('/api/v1/tgm/token-ohlcv', {
1188
1150
  token_address: tokenAddress,
1189
1151
  chain,
@@ -1193,10 +1155,7 @@ export class NansenAPI {
1193
1155
 
1194
1156
  async tokenInformation(params = {}) {
1195
1157
  const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
1196
- if (tokenAddress) {
1197
- const validation = validateTokenAddress(tokenAddress, chain);
1198
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1199
- }
1158
+ if (tokenAddress) requireValidToken(tokenAddress, chain);
1200
1159
  return this.request('/api/v1/tgm/token-information', {
1201
1160
  token_address: tokenAddress,
1202
1161
  chain,
@@ -1279,8 +1238,7 @@ export class NansenAPI {
1279
1238
  async pmTradesByAddress(params = {}) {
1280
1239
  const { address, orderBy, pagination } = params;
1281
1240
  // Polymarket runs exclusively on Polygon
1282
- const validation = validateAddress(address, 'polygon');
1283
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1241
+ requireValidAddress(address, 'polygon');
1284
1242
  return this.request('/api/v1/prediction-market/trades-by-address', {
1285
1243
  address,
1286
1244
  order_by: orderBy,
@@ -1351,8 +1309,7 @@ export class NansenAPI {
1351
1309
  async pmPnlByAddress(params = {}) {
1352
1310
  const { address, orderBy, pagination } = params;
1353
1311
  // Polymarket runs exclusively on Polygon
1354
- const validation = validateAddress(address, 'polygon');
1355
- if (!validation.valid) throw new NansenError(validation.error, validation.code);
1312
+ requireValidAddress(address, 'polygon');
1356
1313
  return this.request('/api/v1/prediction-market/pnl-by-address', {
1357
1314
  address,
1358
1315
  order_by: orderBy,
package/src/cli.js CHANGED
@@ -694,13 +694,13 @@ export async function compareWallets(api, params = {}) {
694
694
 
695
695
  export const BANNER = '';
696
696
 
697
- export const HELP = `Nansen CLI v${VERSION} designed for AI agents.
697
+ export const HELP = `Nansen CLI v${VERSION} - analytics and DEX trading for AI agents.
698
698
 
699
699
  USAGE: nansen <command> [subcommand] [options]
700
700
 
701
701
  COMMANDS:
702
- research smart-money, profiler, token, search, perp, portfolio, points
703
- trade quote, execute
702
+ trade DEX swaps/bridges: quote, execute, bridge-status, limit-order
703
+ research analytics: smart-money, profiler, token, search, perp, portfolio, points
704
704
  wallet create, list, show, export, default, delete, forget-password
705
705
  agent Ask the Nansen AI research agent (fast/expert modes)
706
706
  alerts list, create, update, toggle, delete
@@ -716,11 +716,19 @@ OPTIONS: --chain --limit --sort field:dir --fields a,b --days N --filters '{}'
716
716
  FORMAT: --pretty --table --format csv --stream (NDJSON)
717
717
  RETRY: --no-retry --retries N --cache --cache-ttl N
718
718
 
719
+ TRADING:
720
+ nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
721
+ nansen trade execute --quote <quoteId>
722
+ nansen trade bridge-status --tx-hash <hash> --from-chain base --to-chain solana
723
+ nansen trade limit-order create --from SOL --to USDC --amount 1.5 --trigger-mint SOL --trigger-condition below --trigger-price 80
724
+ Supports Solana/Base DEX swaps, cross-chain bridges, and Solana limit orders.
725
+
719
726
  EXAMPLES:
727
+ nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
728
+ nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
720
729
  nansen research smart-money netflow --chain solana
721
730
  nansen research token screener --chain solana --timeframe 24h
722
731
  nansen research profiler balance --address 0x... --chain ethereum
723
- nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
724
732
 
725
733
  DEPRECATED ALIASES (still work, will be removed in a future version):
726
734
  smart-money, profiler, token, search, perp, portfolio, points → use "nansen research <command>"
@@ -1507,7 +1515,7 @@ EXAMPLES:
1507
1515
  nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
1508
1516
  nansen trade execute --quote 1708900000000-abc123
1509
1517
  nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana
1510
- nansen trade limit-order create --from SOL --to USDC --amount 1000000000 --trigger-mint SOL --trigger-condition below --trigger-price 80
1518
+ nansen trade limit-order create --from SOL --to USDC --amount 1.5 --trigger-mint SOL --trigger-condition below --trigger-price 80
1511
1519
  nansen trade limit-order list
1512
1520
 
1513
1521
  WALLET:
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Nansen CLI - Command-line interface for Nansen API
3
+ * Nansen CLI - Command-line interface for Nansen API analytics and DEX trading
4
4
  * Designed for AI agents.
5
5
  *
6
6
  * Usage: nansen <command> [options]
@@ -11,7 +11,7 @@ import path from 'path';
11
11
  import { base58Encode, exportWallet, getWalletConfig, showWallet } from './wallet.js';
12
12
  import { signEd25519, base58Decode, parseAmount, getTokenInfo } from './transfer.js';
13
13
  import { signSolanaTransaction, resolveTokenAddress } from './trading.js';
14
- import { validateTokenAddress } from './api.js';
14
+ import { validateTokenAddress, telemetryHeaders, packageVersion } from './api.js';
15
15
  import { getWalletConnectAddress, sendSolanaTransactionViaWalletConnect, signSolanaMessageViaWalletConnect } from './walletconnect-trading.js';
16
16
  import { retrievePassword } from './keychain.js';
17
17
  import { CHAIN_RPCS } from './rpc-urls.js';
@@ -88,8 +88,11 @@ async function loFetch(method, endpoint, { token, body, query } = {}) {
88
88
  }
89
89
 
90
90
  const headers = {
91
- 'Accept': 'application/json',
92
91
  'Content-Type': 'application/json',
92
+ 'Accept': 'application/json',
93
+ 'User-Agent': `nansen-cli/${packageVersion}`,
94
+ 'X-Client-Type': 'nansen-cli',
95
+ ...telemetryHeaders(),
93
96
  };
94
97
  if (token) {
95
98
  headers['Authorization'] = `Bearer ${token}`;
package/src/schema.json CHANGED
@@ -951,7 +951,7 @@
951
951
  "amount": {
952
952
  "type": "string",
953
953
  "required": true,
954
- "description": "Amount in base units (e.g. lamports)"
954
+ "description": "Amount in token units (e.g. 1.5 = 1.5 SOL)"
955
955
  },
956
956
  "trigger-price": {
957
957
  "type": "number",
package/src/telemetry.js CHANGED
@@ -61,10 +61,12 @@ const TELEMETRY_ID_FILE = path.join(
61
61
  */
62
62
  let _anonymousId;
63
63
  export function getAnonymousId() {
64
- if (_anonymousId === undefined) {
64
+ if (!_anonymousId) {
65
65
  try {
66
- _anonymousId = fs.readFileSync(TELEMETRY_ID_FILE, 'utf8').trim();
67
- } catch {
66
+ const stored = fs.readFileSync(TELEMETRY_ID_FILE, 'utf8').trim();
67
+ if (stored) _anonymousId = stored;
68
+ } catch { /* missing or unreadable → generate below */ }
69
+ if (!_anonymousId) {
68
70
  _anonymousId = crypto.randomUUID();
69
71
  try {
70
72
  fs.mkdirSync(path.dirname(TELEMETRY_ID_FILE), { recursive: true });
@@ -194,6 +196,7 @@ export function trackCommandSucceeded({
194
196
  timestamp: new Date().toISOString(),
195
197
  path: commandToPath(command),
196
198
  properties: {
199
+ source: `nansen-cli/${cliVersion}`,
197
200
  latency: duration_ms / 1000,
198
201
  from_cache,
199
202
  flags,
@@ -232,6 +235,7 @@ export function trackCommandFailed({
232
235
  timestamp: new Date().toISOString(),
233
236
  path: commandToPath(command),
234
237
  properties: {
238
+ source: `nansen-cli/${cliVersion}`,
235
239
  latency: duration_ms / 1000,
236
240
  error_code,
237
241
  status,
package/src/trading.js CHANGED
@@ -15,7 +15,7 @@ import { getWalletConnectAddress, sendTransactionViaWalletConnect, sendSolanaTra
15
15
  import { retrievePassword } from './keychain.js';
16
16
  import { validateQuoteInput, validateBalance, resolvePercentAmount, validateGasBalance } from './trade-validation.js';
17
17
  import { CHAIN_RPCS } from './rpc-urls.js';
18
- import { packageVersion, CommandError } from './api.js';
18
+ import { packageVersion, CommandError, telemetryHeaders } from './api.js';
19
19
 
20
20
  // ============= Constants =============
21
21
 
@@ -104,6 +104,14 @@ function getQuotesDir() {
104
104
  return path.join(configDir, 'quotes');
105
105
  }
106
106
 
107
+ // Resolve a filename inside the quotes dir, rejecting path traversal.
108
+ function safeQuotesPath(filename) {
109
+ const base = path.resolve(getQuotesDir());
110
+ const target = path.resolve(base, filename);
111
+ if (path.relative(base, target).startsWith('..')) return null;
112
+ return target;
113
+ }
114
+
107
115
  // ============= Trading API Client =============
108
116
 
109
117
  /**
@@ -121,7 +129,7 @@ export async function getQuote(params) {
121
129
  }
122
130
  }
123
131
 
124
- const headers = { 'Accept': 'application/json', 'User-Agent': CLIENT_USER_AGENT };
132
+ const headers = { 'Accept': 'application/json', 'User-Agent': CLIENT_USER_AGENT, 'X-Client-Type': 'nansen-cli', ...telemetryHeaders() };
125
133
 
126
134
  const res = await fetch(url.toString(), { headers });
127
135
 
@@ -160,6 +168,8 @@ export async function executeTransaction(params, { retries = 2, retryDelayMs = 1
160
168
  'Content-Type': 'application/json',
161
169
  'Accept': 'application/json',
162
170
  'User-Agent': CLIENT_USER_AGENT,
171
+ 'X-Client-Type': 'nansen-cli',
172
+ ...telemetryHeaders(),
163
173
  };
164
174
  let lastError;
165
175
  for (let attempt = 0; attempt <= retries; attempt++) {
@@ -234,7 +244,14 @@ export async function getBridgeStatus(txHash, fromChain, toChain, { aggregator,
234
244
  for (let attempt = 0; attempt <= retries; attempt++) {
235
245
  if (attempt > 0) await new Promise(r => setTimeout(r, retryDelayMs));
236
246
 
237
- const res = await fetch(url.toString(), { headers: { 'Accept': 'application/json', 'User-Agent': CLIENT_USER_AGENT } });
247
+ const res = await fetch(url.toString(), {
248
+ headers: {
249
+ 'Accept': 'application/json',
250
+ 'User-Agent': CLIENT_USER_AGENT,
251
+ 'X-Client-Type': 'nansen-cli',
252
+ ...telemetryHeaders(),
253
+ },
254
+ });
238
255
  const text = await res.text();
239
256
  let body;
240
257
  try {
@@ -328,10 +345,12 @@ const TX_RECORD_TTL_MS = 30 * 24 * 3600 * 1000; // 30 days
328
345
  */
329
346
  export function saveTxRecord(txHash, { aggregator, requestId, fromChain, toChain }) {
330
347
  if (!txHash) return;
331
- const dir = getQuotesDir();
348
+ const filePath = safeQuotesPath(`tx-${txHash}.json`);
349
+ if (!filePath) return;
350
+ const dir = path.dirname(filePath);
332
351
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
333
352
  const data = { txHash, aggregator, requestId, fromChain, toChain, timestamp: Date.now() };
334
- fs.writeFileSync(path.join(dir, `tx-${txHash}.json`), JSON.stringify(data, null, 2), { mode: 0o600 });
353
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
335
354
  }
336
355
 
337
356
  /**
@@ -339,7 +358,8 @@ export function saveTxRecord(txHash, { aggregator, requestId, fromChain, toChain
339
358
  */
340
359
  export function loadTxRecord(txHash) {
341
360
  if (!txHash) return null;
342
- const filePath = path.join(getQuotesDir(), `tx-${txHash}.json`);
361
+ const filePath = safeQuotesPath(`tx-${txHash}.json`);
362
+ if (!filePath) return null;
343
363
  if (!fs.existsSync(filePath)) return null;
344
364
  try {
345
365
  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -382,8 +402,8 @@ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalle
382
402
  * Load a saved quote by ID.
383
403
  */
384
404
  export function loadQuote(quoteId) {
385
- const filePath = path.join(getQuotesDir(), `${quoteId}.json`);
386
- if (!fs.existsSync(filePath)) {
405
+ const filePath = safeQuotesPath(`${quoteId}.json`);
406
+ if (!filePath || !fs.existsSync(filePath)) {
387
407
  throw new Error(`Quote "${quoteId}" not found. Quotes expire after 1 hour.`);
388
408
  }
389
409
  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -1997,6 +2017,11 @@ EXAMPLES:
1997
2017
  chain,
1998
2018
  simulate: !noSimulate && !gasless,
1999
2019
  };
2020
+
2021
+ // Include backend quoteId if available for better BI tracking
2022
+ if (currentQuote.metadata?.quoteId) {
2023
+ execParams.quoteId = currentQuote.metadata.quoteId;
2024
+ }
2000
2025
  // The backend's /execute schema is strict; sending fields it doesn't expect
2001
2026
  // for the (chain × aggregator × gasless) combination causes 502s or
2002
2027
  // "Unrecognized keys" rejections. The matrix we've validated against the