nansen-cli 1.9.2 → 1.10.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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#125](https://github.com/nansen-ai/nansen-cli/pull/125) [`5a5a80a`](https://github.com/nansen-ai/nansen-cli/commit/5a5a80af9c2c5b93efcc707925b004f077e13c36) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Add modular skills/ directory with 7 agent-optimised SKILL.md files (nansen-token, nansen-smart-money, nansen-profiler, nansen-trade, nansen-wallet, nansen-perp, nansen-search) following the linear-cli pattern. Each skill has scoped frontmatter, agent routing descriptions, bash examples, and exit codes. Add skills nudge to `nansen --help` output.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#122](https://github.com/nansen-ai/nansen-cli/pull/122) [`9a1ada8`](https://github.com/nansen-ai/nansen-cli/commit/9a1ada8543cd6fdbcc10d2d5004fe2e2e1a88928) Thanks [@TimNooren](https://github.com/TimNooren)! - `nansen research <unknown>` and `nansen trade <unknown>` now exit with code 1 and return `{"success":false,...}` instead of silently exiting 0.
12
+
13
+ - [#138](https://github.com/nansen-ai/nansen-cli/pull/138) [`c61881f`](https://github.com/nansen-ai/nansen-cli/commit/c61881f5455b9fff7fb97841652a72af58ab8e0b) Thanks [@TimNooren](https://github.com/TimNooren)! - Fix `nansen login --help` to show usage instead of erroring. Previously, `--help` was silently ignored on TTY (showing the interactive prompt) and caused an error on non-TTY. Also fixes the post-login suggested command to use the non-deprecated `nansen research token screener` path.
14
+
15
+ - [#129](https://github.com/nansen-ai/nansen-cli/pull/129) [`eeabf89`](https://github.com/nansen-ai/nansen-cli/commit/eeabf8988dafc7fb1964fd2eef629f03c6a4420a) Thanks [@araa47](https://github.com/araa47)! - Fix `token ohlcv` sending unsupported pagination/limit params that caused 422 errors
16
+
17
+ - [#139](https://github.com/nansen-ai/nansen-cli/pull/139) [`e86dc68`](https://github.com/nansen-ai/nansen-cli/commit/e86dc6869f524d3dc59da4c7c04cb1ace1b7246b) Thanks [@TimNooren](https://github.com/TimNooren)! - Fix API key prompt masking: each keystroke was showing the real character followed by `*` (e.g. `f*o*o*`) because the readline interface was active alongside raw mode, causing double output. Moving readline creation into the non-hidden branch eliminates the double-echo and also fixes backspace incorrectly clearing the prompt label.
18
+
19
+ - [#129](https://github.com/nansen-ai/nansen-cli/pull/129) [`eeabf89`](https://github.com/nansen-ai/nansen-cli/commit/eeabf8988dafc7fb1964fd2eef629f03c6a4420a) Thanks [@araa47](https://github.com/araa47)! - Fix `trade quote` crash when no wallet exists — now shows actionable error instead of uncaught exception
20
+
21
+ - [#126](https://github.com/nansen-ai/nansen-cli/pull/126) [`f3b87e7`](https://github.com/nansen-ai/nansen-cli/commit/f3b87e7491d03d052d5d72fcc991de0c33caf51f) Thanks [@araa47](https://github.com/araa47)! - Remove root SKILL.md so `npx skills add nansen-ai/nansen-cli` correctly discovers all 7 skills in `skills/` instead of treating the repo as a single skill.
22
+
23
+ ## 1.9.3
24
+
25
+ ### Patch Changes
26
+
27
+ - [#118](https://github.com/nansen-ai/nansen-cli/pull/118) [`0bd4c3c`](https://github.com/nansen-ai/nansen-cli/commit/0bd4c3c1946e575e2c2db5e02d17f266e79752a4) Thanks [@TimNooren](https://github.com/TimNooren)! - Show warning when trade quote price impact exceeds 5%, and show pin command to avoid fallback to worse quotes
28
+
3
29
  ## 1.9.2
4
30
 
5
31
  ### Patch Changes
package/README.md CHANGED
@@ -2,226 +2,56 @@
2
2
 
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
- [![CI](https://github.com/nansen-ai/nansen-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/nansen-ai/nansen-cli/actions/workflows/ci.yml)
6
5
 
7
- > **Built by agents, for agents.** We prioritize the best possible AI agent experience.
8
-
9
- Command-line interface for the [Nansen API](https://docs.nansen.ai) with structured JSON output, designed for AI agents and automation.
6
+ > **Built by agents, for agents.** Command-line interface for the [Nansen API](https://docs.nansen.ai) with structured JSON output.
10
7
 
11
8
  ## Installation
12
9
 
13
10
  ```bash
14
- # Install globally via npm
15
11
  npm install -g nansen-cli
16
-
17
- # Or run directly with npx
18
- npx nansen-cli help
19
-
20
- # Or clone and install locally
21
- git clone https://github.com/nansen-ai/nansen-cli.git
22
- cd nansen-cli
23
- npm install
24
- npm link
12
+ npx skills add nansen-ai/nansen-cli # load agent skill files
25
13
  ```
26
14
 
27
- ## Configuration
28
-
29
- ### For AI Agents (Recommended)
30
-
31
- Use the [AI Agent Setup](https://app.nansen.ai/auth/agent-setup) flow:
32
-
33
- 1. Your agent will ask you to visit: **[app.nansen.ai/auth/agent-setup](https://app.nansen.ai/auth/agent-setup)**
34
- 2. Sign in with your Nansen account
35
- 3. Copy the message shown
36
- 4. Paste it back to your agent
37
-
38
- Your agent saves the key and handles everything else automatically.
39
-
40
- ### Manual Setup
41
-
42
- **Option 1: Interactive login**
43
- ```bash
44
- nansen login
45
- # Enter your API key when prompted
46
- # ✓ Saved to ~/.nansen/config.json
47
- ```
48
-
49
- **Option 2: Environment variable (best for agents)**
50
- ```bash
51
- export NANSEN_API_KEY=your-api-key
52
- ```
53
-
54
- **Option 3: Direct config file**
55
- ```bash
56
- mkdir -p ~/.nansen && echo '{"apiKey":"<key>","baseUrl":"https://api.nansen.ai"}' > ~/.nansen/config.json && chmod 600 ~/.nansen/config.json
57
- ```
58
-
59
- Get your API key at [app.nansen.ai/api](https://app.nansen.ai/api).
60
-
61
- ### Auth Priority
62
-
63
- 1. `NANSEN_API_KEY` env var (highest)
64
- 2. `~/.nansen/config.json` file
65
- 3. Interactive prompt
66
-
67
- ### Verify It Works
15
+ ## Auth
68
16
 
69
17
  ```bash
70
- # Check CLI is installed (no API key needed):
71
- nansen schema | head -1
72
-
73
- # Verify API access:
74
- nansen research token screener --chain solana --limit 1
18
+ nansen login # interactive saves to ~/.nansen/config.json
19
+ export NANSEN_API_KEY=... # or env var (highest priority)
75
20
  ```
76
21
 
77
- ## Quick Start
78
-
79
- ```bash
80
- # Get trending tokens on Solana
81
- nansen research token screener --chain solana --timeframe 24h --pretty
82
-
83
- # Check Smart Money activity
84
- nansen research smart-money netflow --chain solana --pretty
85
-
86
- # Profile a wallet
87
- nansen research profiler balance --address 0x28c6c06298d514db089934071355e5743bf21d60 --chain ethereum --pretty
88
-
89
- # Search for tokens/entities
90
- nansen research search "Vitalik Buterin" --pretty
91
- ```
22
+ Get your API key at [app.nansen.ai/api](https://app.nansen.ai/api). AI agents can use the [Agent Setup](https://app.nansen.ai/auth/agent-setup) flow instead.
92
23
 
93
24
  ## Commands
94
25
 
95
- All analytics live under `nansen research`, trading under `nansen trade`, and wallet management under `nansen wallet`.
96
-
97
- ### `research` - Research & Analytics
98
-
99
26
  ```
100
27
  nansen research <category> <subcommand> [options]
28
+ nansen trade <subcommand> [options]
29
+ nansen wallet <subcommand> [options]
30
+ nansen schema [command] [--pretty] # full command reference (no API key needed)
101
31
  ```
102
32
 
103
- **Category aliases:** `sm` (smart-money), `tgm` (token), `prof` (profiler), `port` (portfolio)
104
-
105
- #### `research smart-money` - Smart Money Analytics
106
-
107
- Track trading and holding activity of sophisticated market participants.
33
+ **Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `search`, `perp`, `points`
108
34
 
109
- | Subcommand | Description |
110
- |------------|-------------|
111
- | `netflow` | Net capital flows (inflows vs outflows) |
112
- | `dex-trades` | Real-time DEX trading activity |
113
- | `perp-trades` | Perpetual trading on Hyperliquid |
114
- | `holdings` | Aggregated token balances |
115
- | `dcas` | DCA strategies on Jupiter |
116
- | `historical-holdings` | Historical holdings over time |
35
+ **Trade:** `quote`, `execute` DEX swaps via LiFi/Jupiter.
117
36
 
118
- **Smart Money Labels:** `Fund`, `Smart Trader`, `30D Smart Trader`, `90D Smart Trader`, `180D Smart Trader`, `Smart HL Perps Trader`
37
+ **Wallet:** `create`, `list`, `show`, `export`, `default`, `delete`, `send` local keypairs (EVM + Solana).
119
38
 
120
- #### `research profiler` - Wallet Profiling
39
+ Run `nansen schema --pretty` for the full subcommand and field reference.
121
40
 
122
- **ENS Name Resolution:** You can use `.eth` names anywhere an `--address` is accepted:
123
-
124
- ```bash
125
- nansen research profiler balance --address vitalik.eth
126
- nansen research profiler labels --address nansen.eth --chain ethereum
127
- nansen research profiler transactions --address vitalik.eth --table
128
- ```
129
-
130
- ENS names are automatically resolved to `0x` addresses via public APIs (with onchain RPC fallback). Works on all EVM chains. The resolved name and address are included as `_ens` metadata in JSON output.
131
-
132
- | Subcommand | Description |
133
- |------------|-------------|
134
- | `balance` | Current token holdings |
135
- | `labels` | Behavioral and entity labels |
136
- | `transactions` | Transaction history |
137
- | `pnl` | PnL and trade performance |
138
- | `search` | Search for entities by name |
139
- | `historical-balances` | Historical balances over time |
140
- | `related-wallets` | Find wallets related to an address |
141
- | `counterparties` | Top counterparties by volume |
142
- | `pnl-summary` | Summarized PnL metrics |
143
- | `perp-positions` | Current perpetual positions |
144
- | `perp-trades` | Perpetual trading history |
145
-
146
- #### `research token` - Token God Mode
147
-
148
- | Subcommand | Description |
149
- |------------|-------------|
150
- | `screener` | Discover and filter tokens |
151
- | `holders` | Token holder analysis |
152
- | `flows` | Token flow metrics |
153
- | `dex-trades` | DEX trading activity |
154
- | `pnl` | PnL leaderboard |
155
- | `who-bought-sold` | Recent buyers and sellers |
156
- | `flow-intelligence` | Detailed flow intelligence by label |
157
- | `transfers` | Token transfer history |
158
- | `jup-dca` | Jupiter DCA orders for token |
159
- | `ohlcv` | OHLCV candle data for a token |
160
- | `perp-trades` | Perp trades by token symbol |
161
- | `perp-positions` | Open perp positions by token symbol |
162
- | `perp-pnl-leaderboard` | Perp PnL leaderboard by token |
163
-
164
- #### `research search` / `research perp` / `research portfolio` / `research points`
165
-
166
- See `nansen research help` or `nansen schema --pretty` for full details.
167
-
168
- ### `trade` - DEX Trading
169
-
170
- ```bash
171
- # Get a swap quote
172
- nansen trade quote --from USDC --to SOL --amount 10 --chain solana
173
-
174
- # Execute the swap
175
- nansen trade execute --from USDC --to SOL --amount 10 --chain solana
176
- ```
177
-
178
- ### `wallet` - Local Wallet Management
179
-
180
- | Subcommand | Description |
181
- |------------|-------------|
182
- | `create` | Create a new wallet (EVM + Solana keypair) |
183
- | `list` | List all wallets |
184
- | `show` | Show wallet addresses |
185
- | `export` | Export private keys |
186
- | `default` | Set default wallet |
187
- | `delete` | Delete a wallet |
188
- | `send` | Send tokens (native or ERC-20/SPL) |
189
-
190
- Wallets are passwordless by default (keys stored like SSH keys). Set `NANSEN_WALLET_PASSWORD` env var for encryption at rest.
191
-
192
- ### `schema` - Schema Discovery
193
-
194
- No API key required. Machine-readable command reference for agent introspection.
195
-
196
- ```bash
197
- nansen schema --pretty # All commands
198
- nansen schema research --pretty # Research commands
199
- ```
200
-
201
- ### Deprecated Flat Commands
202
-
203
- The old flat commands (`nansen smart-money`, `nansen token`, `nansen profiler`, `nansen search`, `nansen perp`, `nansen portfolio`, `nansen points`, `nansen quote`, `nansen execute`) still work but print a deprecation warning to stderr. Use the new `research` and `trade` namespaces instead.
204
-
205
- ## Options
41
+ ## Key Options
206
42
 
207
43
  | Option | Description |
208
44
  |--------|-------------|
209
- | `--pretty` | Format JSON output for readability |
210
- | `--table` | Format output as human-readable table |
211
- | `--fields <list>` | Comma-separated fields to include (reduces response size) |
212
- | `--stream` | Output as NDJSON for incremental processing |
213
- | `--cache` / `--no-cache` | Enable/disable response caching |
214
- | `--cache-ttl <s>` | Cache TTL in seconds (default: 300) |
215
45
  | `--chain <chain>` | Blockchain to query |
216
- | `--chains <json>` | Multiple chains as JSON array |
217
- | `--limit <n>` | Number of results |
218
- | `--days <n>` | Date range in days (default: 30) |
219
- | `--sort <field:dir>` | Sort results (e.g., `--sort value_usd:desc`) |
220
- | `--symbol <sym>` | Token symbol for perp endpoints (e.g., BTC, ETH) |
221
- | `--filters <json>` | Filter criteria as JSON |
46
+ | `--limit <n>` | Result count |
47
+ | `--timeframe <tf>` | Time window: `5m` `1h` `6h` `24h` `7d` `30d` |
48
+ | `--fields <list>` | Comma-separated fields (reduces response size) |
49
+ | `--sort <field:dir>` | Sort results, e.g. `--sort value_usd:desc` |
50
+ | `--pretty` | Human-readable JSON |
51
+ | `--table` | Table format |
52
+ | `--stream` | NDJSON output for large results |
222
53
  | `--labels <label>` | Smart Money label filter |
223
- | `--smart-money` | Filter for Smart Money only |
224
- | `--timeframe <tf>` | Time window (5m, 10m, 1h, 6h, 24h, 7d, 30d) |
54
+ | `--smart-money` | Filter for Smart Money addresses only |
225
55
 
226
56
  ## Supported Chains
227
57
 
@@ -229,121 +59,51 @@ The old flat commands (`nansen smart-money`, `nansen token`, `nansen profiler`,
229
59
 
230
60
  > Run `nansen schema` to get the current chain list (source of truth).
231
61
 
232
- ## Agent-Optimized Patterns
233
-
234
- ### Reduce Token Burn with `--fields`
235
-
236
- ```bash
237
- # ❌ Returns everything (huge JSON, wastes agent context)
238
- nansen research smart-money netflow --chain solana
239
-
240
- # ✅ Only what you need
241
- nansen research smart-money netflow --chain solana --fields token_symbol,net_flow_usd,chain --limit 10
242
- ```
243
-
244
- ### Use `--stream` for Large Results
62
+ ## Agent Tips
245
63
 
64
+ **Reduce token burn with `--fields`:**
246
65
  ```bash
247
- # NDJSON mode process line by line, don't buffer giant arrays
248
- nansen research token dex-trades --chain solana --limit 100 --stream
66
+ nansen research smart-money netflow --chain solana --fields token_symbol,net_flow_usd --limit 10
249
67
  ```
250
68
 
251
- ### x402 Micropayments
69
+ **Use `--stream` for large results** — outputs NDJSON instead of buffering a giant array.
252
70
 
253
- When the API returns a 402 Payment Required, the CLI automatically handles payment if a funded wallet exists:
254
-
255
- 1. CLI detects 402 response with payment requirements
256
- 2. Signs a USDC payment ($0.05/call) using your wallet
257
- 3. Retries the request with the payment signature
258
- 4. Falls back from EVM to Solana if first network has insufficient funds
259
-
260
- ```bash
261
- # Fund your wallet, then API calls auto-pay
262
- nansen wallet create
263
- # Send USDC to the displayed address
264
- nansen research smart-money netflow --chain solana # auto-pays if no API key
265
- ```
266
-
267
- ## Pagination
268
-
269
- Use `--limit N` to control result count. The CLI always fetches page 1 (there is no `--page` flag).
270
-
271
- **Detecting the last page:** If results returned < your `--limit`, you've reached the end.
71
+ **ENS names** work anywhere `--address` is accepted: `--address vitalik.eth`
272
72
 
273
73
  ## Output Format
274
74
 
275
- ### Response envelope
276
-
277
75
  ```json
278
- // Success
279
- { "success": true, "data": <raw_api_response> }
280
-
281
- // Error
282
- { "success": false, "error": "message", "code": "ERROR_CODE", "status": 401, "details": {...} }
76
+ { "success": true, "data": <api_response> }
77
+ { "success": false, "error": "message", "code": "ERROR_CODE", "status": 401 }
283
78
  ```
284
79
 
285
- ### Response shapes vary by endpoint
286
-
287
- The `data` field structure differs across endpoints:
288
-
289
- | Shape | Example endpoints |
290
- |-------|------------------|
291
- | `data.data` (array) | token screener |
292
- | `data.results` (array) | entity search |
293
- | `data.data.results` (array) | most profiler endpoints |
294
- | `data.netflows` | smart-money netflow |
295
- | `data.trades` | smart-money dex-trades |
296
- | `data.holdings` | smart-money holdings |
297
- | `data.holders` | token holders |
298
-
299
- `--table` and `--stream` handle this automatically. For raw JSON parsing:
300
-
301
- ```bash
302
- nansen research smart-money netflow --chain solana | jq 'keys, .data | keys'
303
- ```
304
-
305
- ### Error codes
80
+ **Critical error codes:**
306
81
 
307
82
  | Code | Action |
308
83
  |------|--------|
309
- | `CREDITS_EXHAUSTED` | Stop all API calls immediately — do not retry. Check your plan at [app.nansen.ai](https://app.nansen.ai). |
310
- | `RATE_LIMITED` | Auto-retry handles this. |
84
+ | `CREDITS_EXHAUSTED` | Stop all API calls immediately. Check [app.nansen.ai](https://app.nansen.ai). |
85
+ | `UNAUTHORIZED` | Wrong or missing key. Re-auth. |
86
+ | `RATE_LIMITED` | Auto-retried by CLI. |
311
87
  | `UNSUPPORTED_FILTER` | Remove the filter and retry. |
312
- | `UNAUTHORIZED` | Key is wrong or missing. Re-auth. |
313
- | `INVALID_ADDRESS` | Check address format for the chain. |
314
88
 
315
89
  ## Troubleshooting
316
90
 
317
91
  | Symptom | Fix |
318
92
  |---------|-----|
319
- | `command not found: nansen` | `npm install -g nansen-cli` or `npx nansen-cli` |
320
- | `UNAUTHORIZED` after login | Check `cat ~/.nansen/config.json`. Write directly if needed. |
321
- | Login hangs | Skip `nansen login`, write config directly. |
322
- | Huge JSON response | Use `--fields` to select only needed columns. |
323
- | Perp endpoints empty | Use `--symbol BTC` not `--token`. Perps are Hyperliquid-only. |
324
- | `UNSUPPORTED_FILTER` on token holders | Not all tokens have smart money data. Remove `--smart-money`. |
325
- | `CREDITS_EXHAUSTED` | Check your plan at [app.nansen.ai](https://app.nansen.ai). |
93
+ | `command not found` | `npm install -g nansen-cli` |
94
+ | `UNAUTHORIZED` after login | `cat ~/.nansen/config.json` or set `NANSEN_API_KEY` |
95
+ | Empty perp results | Use `--symbol BTC`, not `--token`. Perps are Hyperliquid-only. |
96
+ | `UNSUPPORTED_FILTER` on token holders | Remove `--smart-money` not all tokens have that data. |
97
+ | Huge JSON response | Use `--fields` to select columns. |
326
98
 
327
99
  ## Development
328
100
 
329
101
  ```bash
330
- npm test # Run tests (mocked, no API key needed)
331
- npm run test:coverage # With coverage
332
- npm run test:live # Against live API (needs NANSEN_API_KEY)
102
+ npm test # mocked tests, no API key needed
103
+ npm run test:live # live API (needs NANSEN_API_KEY)
333
104
  ```
334
105
 
335
- See [AGENTS.md](AGENTS.md) for contributor guidance (architecture, testing patterns, style guide).
336
-
337
- ## API Coverage
338
-
339
- | Category | Endpoints | Coverage |
340
- |----------|-----------|----------|
341
- | Smart Money | 6 | 100% |
342
- | Profiler | 11 | 100% |
343
- | Token God Mode | 12 | 100% |
344
- | Portfolio | 1 | 100% |
345
- | Search | 1 | 100% |
346
- | **Total** | **31** | **100%** |
106
+ See [AGENTS.md](AGENTS.md) for architecture and contributor guidance.
347
107
 
348
108
  ## License
349
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.9.2",
3
+ "version": "1.10.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -18,7 +18,9 @@
18
18
  "test:watch": "vitest",
19
19
  "test:coverage": "vitest run --coverage",
20
20
  "test:live": "NANSEN_LIVE_TEST=1 vitest run",
21
- "test:swap": "vitest run --config vitest.e2e.config.js",
21
+ "test:trade": "vitest run --config vitest.e2e.config.js",
22
+ "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
22
24
  "changeset": "changeset",
23
25
  "changeset:version": "changeset version",
24
26
  "changeset:publish": "changeset publish"
@@ -52,7 +54,10 @@
52
54
  "devDependencies": {
53
55
  "@changesets/changelog-github": "^0.5.1",
54
56
  "@changesets/cli": "^2.29.4",
57
+ "@eslint/js": "^10.0.1",
55
58
  "@vitest/coverage-v8": "^4.0.18",
59
+ "eslint": "^10.0.2",
60
+ "globals": "^17.4.0",
56
61
  "vitest": "^4.0.18"
57
62
  }
58
63
  }
package/src/api.js CHANGED
@@ -194,9 +194,9 @@ export function getCachedResponse(endpoint, body, ttlSeconds = DEFAULT_CACHE_TTL
194
194
  }
195
195
 
196
196
  return { ...cached.data, _meta: { ...cached.data._meta, fromCache: true, cacheAge: Math.round(age) } };
197
- } catch (e) {
197
+ } catch (_e) {
198
198
  // Invalid cache file, delete it
199
- try { fs.unlinkSync(cacheFile); } catch {}
199
+ try { fs.unlinkSync(cacheFile); } catch { /* ignore */ }
200
200
  return null;
201
201
  }
202
202
  }
@@ -305,7 +305,7 @@ function loadConfig() {
305
305
 
306
306
  // ~/.nansen/config.json (from `nansen login`)
307
307
  if (fs.existsSync(CONFIG_FILE)) {
308
- try { config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch (e) {}
308
+ try { config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch (_e) { /* ignore */ }
309
309
  }
310
310
 
311
311
  // Local config.json (for development)
@@ -476,7 +476,7 @@ export class NansenAPI {
476
476
  let data;
477
477
  try {
478
478
  data = await response.json();
479
- } catch (err) {
479
+ } catch (_err) {
480
480
  // Non-JSON response (rare, usually server errors)
481
481
  const error = new NansenError(
482
482
  `Invalid response from API (status ${response.status})`,
@@ -1039,7 +1039,7 @@ export class NansenAPI {
1039
1039
  }
1040
1040
 
1041
1041
  async tokenOhlcv(params = {}) {
1042
- const { tokenAddress, chain = 'solana', timeframe, pagination } = params;
1042
+ const { tokenAddress, chain = 'solana', timeframe } = params;
1043
1043
  if (tokenAddress) {
1044
1044
  const validation = validateTokenAddress(tokenAddress, chain);
1045
1045
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
@@ -1048,7 +1048,6 @@ export class NansenAPI {
1048
1048
  token_address: tokenAddress,
1049
1049
  chain,
1050
1050
  timeframe,
1051
- pagination
1052
1051
  });
1053
1052
  }
1054
1053
 
package/src/cli.js CHANGED
@@ -88,7 +88,7 @@ export const SCHEMA = {
88
88
  description: 'Token God Mode - deep analytics for any token',
89
89
  subcommands: {
90
90
  'indicators': { description: 'Risk and reward indicators for a token (Nansen Score)', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'ethereum' } }, returns: ['token_info[market_cap_usd, market_cap_group, is_stablecoin]', 'risk_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]', 'reward_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]'] },
91
- 'ohlcv': { description: 'OHLCV candle data for a token', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'solana' }, timeframe: { type: 'string', default: '1h', enum: ['1m', '5m', '15m', '30m', '1h', '2h', '4h', '1d', '1w', '1M'], description: 'Candle timeframe (e.g., 1h, 4h, 1d)' }, limit: { type: 'number' } }, returns: ['timestamp', 'open', 'high', 'low', 'close', 'volume'] },
91
+ 'ohlcv': { description: 'OHLCV candle data for a token', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'solana' }, timeframe: { type: 'string', default: '1h', enum: ['1m', '5m', '15m', '30m', '1h', '2h', '4h', '1d', '1w', '1M'], description: 'Candle timeframe (e.g., 1h, 4h, 1d)' } }, returns: ['timestamp', 'open', 'high', 'low', 'close', 'volume'] },
92
92
  'info': { description: 'Get detailed information for a specific token', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'solana' }, timeframe: { type: 'string', default: '1d', enum: ['5m', '1h', '6h', '12h', '1d', '7d'] } }, returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd'] },
93
93
  'screener': { description: 'Discover and filter tokens', options: { chain: { type: 'string', default: 'solana' }, chains: { type: 'array' }, timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] }, 'smart-money': { type: 'boolean', description: 'Filter for Smart Money only' }, search: { type: 'string', description: 'Filter results by token symbol or name (client-side)' }, limit: { type: 'number' }, sort: { type: 'string' } }, returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'smart_money_holders'] },
94
94
  'holders': { description: 'Token holder analysis', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, 'smart-money': { type: 'boolean' }, limit: { type: 'number' } }, returns: ['address', 'address_label', 'token_amount', 'total_outflow', 'total_inflow', 'balance_change_24h', 'balance_change_7d', 'balance_change_30d', 'ownership_percentage', 'value_usd'] },
@@ -220,7 +220,52 @@ export function parseFields(fieldsOption) {
220
220
  return fieldsOption.split(',').map(f => f.trim()).filter(f => f.length > 0);
221
221
  }
222
222
 
223
- // Parse command line arguments
223
+ /**
224
+ * Produce a compact schema listing commands with params* notation.
225
+ * Use `nansen schema --full` for the verbose version.
226
+ */
227
+ export function compactSchema(schema) {
228
+ function compactOptions(opts) {
229
+ if (!opts) return '';
230
+ return Object.entries(opts)
231
+ .map(([name, o]) => `${name}${o.required ? '*' : ''}`)
232
+ .join(', ');
233
+ }
234
+
235
+ function compactCmd(prefix, cmd) {
236
+ const entries = [];
237
+ if (cmd.subcommands) {
238
+ for (const [name, sub] of Object.entries(cmd.subcommands)) {
239
+ const path = prefix ? `${prefix} ${name}` : name;
240
+ if (sub.subcommands) {
241
+ entries.push(...compactCmd(path, sub));
242
+ } else {
243
+ const params = compactOptions(sub.options);
244
+ entries.push({ command: path, description: sub.description, params, returns: sub.returns });
245
+ }
246
+ }
247
+ } else {
248
+ const params = compactOptions(cmd.options);
249
+ entries.push({ command: prefix, description: cmd.description, params, returns: cmd.returns });
250
+ }
251
+ return entries;
252
+ }
253
+
254
+ const commands = [];
255
+ for (const [name, cmd] of Object.entries(schema.commands)) {
256
+ commands.push(...compactCmd(name, cmd));
257
+ }
258
+
259
+ return {
260
+ version: schema.version,
261
+ params_legend: '* = required',
262
+ commands,
263
+ globalOptions: Object.keys(schema.globalOptions).join(', '),
264
+ chains: schema.chains,
265
+ smartMoneyLabels: schema.smartMoneyLabels
266
+ };
267
+ }
268
+
224
269
  /**
225
270
  * Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
226
271
  */
@@ -244,7 +289,7 @@ export function parseArgs(args) {
244
289
  const key = arg.slice(2);
245
290
  const next = args[i + 1];
246
291
 
247
- if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich') {
292
+ if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full') {
248
293
  result.flags[key] = true;
249
294
  } else if (next && !next.startsWith('-')) {
250
295
  // Try to parse as JSON first (for objects/arrays/booleans),
@@ -635,7 +680,7 @@ export async function traceCounterparties(api, params = {}) {
635
680
  queue.push({ addr: cpAddr, hop: hop + 1 });
636
681
  }
637
682
  }
638
- } catch (err) {
683
+ } catch {
639
684
  // Skip addresses that fail (404, etc) but continue the traversal
640
685
  }
641
686
 
@@ -706,91 +751,41 @@ export async function compareWallets(api, params = {}) {
706
751
  };
707
752
  }
708
753
 
709
- // ASCII Art Banner
710
- export const BANNER = `
711
- ███╗ ██╗ █████╗ ███╗ ██╗███████╗███████╗███╗ ██╗
712
- ████╗ ██║██╔══██╗████╗ ██║██╔════╝██╔════╝████╗ ██║
713
- ██╔██╗ ██║███████║██╔██╗ ██║███████╗█████╗ ██╔██╗ ██║
714
- ██║╚██╗██║██╔══██║██║╚██╗██║╚════██║██╔══╝ ██║╚██╗██║
715
- ██║ ╚████║██║ ██║██║ ╚████║███████║███████╗██║ ╚████║
716
- ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═══╝
717
- Surface The Signal
718
- `;
754
+ export const BANNER = '';
719
755
 
720
- // Help text
721
- export const HELP = `
722
- Nansen CLI - Command-line interface for Nansen API
723
- Designed for AI agents with structured JSON output.
756
+ export const HELP = `Nansen CLI v${VERSION} — structured JSON output for AI agents.
724
757
 
725
- USAGE:
726
- nansen <command> [subcommand] [options]
758
+ USAGE: nansen <command> [subcommand] [options]
727
759
 
728
760
  COMMANDS:
729
- research Research & analytics (smart-money, profiler, token, search, perp, portfolio, points)
730
- trade DEX trading (quote, execute)
731
- wallet Local wallet management (create, list, show, export, default, delete)
732
- login/logout API key management
733
- schema Output JSON schema for all commands (for agent introspection)
734
- cache Cache management (clear)
735
- changelog Show what's new (use --since <version> to filter)
736
- help Show this help message
737
-
738
- GLOBAL OPTIONS:
739
- --pretty Format JSON output for readability
740
- --table Format output as human-readable table
741
- --fields Comma-separated list of fields to include (e.g., --fields address,value_usd)
742
- --chain Blockchain to query (ethereum, solana, base, etc.)
743
- --chains Multiple chains as JSON array
744
- --limit Number of results (shorthand for pagination)
745
- --filters JSON object with filters
746
- --sort Sort by field (e.g., --sort value_usd:desc)
747
- --order-by JSON array with sort order (advanced)
748
- --days Date range in days (default: 30 for most endpoints)
749
- --symbol Token symbol (for perp endpoints)
750
- --no-retry Disable automatic retry on rate limits/errors
751
- --retries <n> Max retry attempts (default: 3)
752
- --x402-payment-signature <sig> Pre-signed x402 payment signature header
753
- --cache Enable response caching (default: off)
754
- --no-cache Disable cache for this request
755
- --cache-ttl <s> Cache TTL in seconds (default: 300)
756
- --stream Output as JSON lines (NDJSON) for incremental processing
757
- --format csv Output as CSV with header row
761
+ research smart-money, profiler, token, search, perp, portfolio, points
762
+ trade quote, execute
763
+ wallet create, list, show, export, default, delete
764
+ login Save API key (--api-key <key> or interactive)
765
+ logout Remove saved API key
766
+ schema JSON schema for all commands (use "nansen schema <cmd>" for one)
767
+ cache clear
768
+ changelog --since <version> to filter
769
+
770
+ OPTIONS: --chain --limit --sort field:dir --fields a,b --days N --filters '{}'
771
+ FORMAT: --pretty --table --format csv --stream (NDJSON)
772
+ RETRY: --no-retry --retries N --cache --cache-ttl N
758
773
 
759
774
  EXAMPLES:
760
- # Get Smart Money netflow on Solana
761
775
  nansen research smart-money netflow --chain solana
762
-
763
- # Get top tokens by Smart Money activity
764
- nansen research token screener --chain solana --timeframe 24h --pretty
765
-
766
- # Get wallet balance
767
- nansen research profiler balance --address 0x123... --chain ethereum
768
-
769
- # Search for tokens/entities
770
- nansen research search "Vitalik"
771
-
772
- # Get a DEX swap quote
776
+ nansen research token screener --chain solana --timeframe 24h
777
+ nansen research profiler balance --address 0x... --chain ethereum
773
778
  nansen trade quote --chain ethereum --from ETH --to USDC --amount 1
774
779
 
775
- SMART MONEY LABELS:
776
- Fund, Smart Trader, 30D Smart Trader, 90D Smart Trader,
777
- 180D Smart Trader, Smart HL Perps Trader
780
+ Chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
781
+ Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
778
782
 
779
- SUPPORTED CHAINS:
780
- ethereum, solana, base, bnb, arbitrum, polygon, optimism,
781
- avalanche, linea, scroll, mantle, ronin, sei,
782
- plasma, sonic, monad, hyperevm, iotaevm
783
-
784
- For more info: https://docs.nansen.ai
783
+ Docs: https://docs.nansen.ai
784
+ Skills: npx skills add nansen-ai/nansen-cli (agent-optimised docs per command group)
785
785
  `;
786
786
 
787
787
  // Helper to prompt for input (exported for mocking)
788
788
  export async function prompt(question, hidden = false) {
789
- const rl = readline.createInterface({
790
- input: process.stdin,
791
- output: process.stdout
792
- });
793
-
794
789
  return new Promise((resolve) => {
795
790
  if (hidden && process.stdout.isTTY) {
796
791
  process.stdout.write(question);
@@ -805,7 +800,6 @@ export async function prompt(question, hidden = false) {
805
800
  process.stdin.pause();
806
801
  process.stdin.removeListener('data', onData);
807
802
  process.stdout.write('\n');
808
- rl.close();
809
803
  resolve(input);
810
804
  } else if (char === '\u0003') {
811
805
  // Ctrl+C
@@ -824,6 +818,10 @@ export async function prompt(question, hidden = false) {
824
818
 
825
819
  process.stdin.on('data', onData);
826
820
  } else {
821
+ const rl = readline.createInterface({
822
+ input: process.stdin,
823
+ output: process.stdout
824
+ });
827
825
  rl.question(question, (answer) => {
828
826
  rl.close();
829
827
  resolve(answer);
@@ -836,11 +834,11 @@ export async function prompt(question, hidden = false) {
836
834
  export function buildCommands(deps = {}) {
837
835
  // Allow dependency injection for testing
838
836
  const {
839
- api = null,
837
+ api: _api = null,
840
838
  promptFn = prompt,
841
839
  log = console.log,
842
- errorOutput = console.error,
843
- NansenAPIClass = NansenAPI,
840
+ errorOutput: _errorOutput = console.error,
841
+ NansenAPIClass: _NansenAPIClass = NansenAPI,
844
842
  saveConfigFn = saveConfig,
845
843
  deleteConfigFn = deleteConfig,
846
844
  getConfigFileFn = getConfigFile,
@@ -850,6 +848,18 @@ export function buildCommands(deps = {}) {
850
848
 
851
849
  const cmds = {
852
850
  'login': async (args, apiInstance, flags, options) => {
851
+ if (flags.help || flags.h) {
852
+ log('nansen login - Save your Nansen API key\n');
853
+ log('USAGE:');
854
+ log(' nansen login (interactive)');
855
+ log(' nansen login --api-key <key> (non-interactive)\n');
856
+ log('OPTIONS:');
857
+ log(' --api-key <key> Your Nansen API key');
858
+ log(' --help Show this help\n');
859
+ log('Get your API key at: https://app.nansen.ai/api');
860
+ return;
861
+ }
862
+
853
863
  // Support non-interactive: nansen login --api-key <key>
854
864
  let apiKey = options['api-key'] || options.apiKey;
855
865
 
@@ -885,10 +895,10 @@ export function buildCommands(deps = {}) {
885
895
 
886
896
  log(`✓ Saved to ${getConfigFileFn()}\n`);
887
897
  log('You can now use the Nansen CLI. Try:');
888
- log(' nansen token screener --chain solana --pretty');
898
+ log(' nansen research token screener --chain solana --pretty');
889
899
  },
890
900
 
891
- 'logout': async (args, apiInstance, flags, options) => {
901
+ 'logout': async (_args, _apiInstance, _flags, _options) => {
892
902
  const deleted = deleteConfigFn();
893
903
  if (deleted) {
894
904
  log(`✓ Removed ${getConfigFileFn()}`);
@@ -897,11 +907,11 @@ export function buildCommands(deps = {}) {
897
907
  }
898
908
  },
899
909
 
900
- 'help': async (args, apiInstance, flags, options) => {
910
+ 'help': async (_args, _apiInstance, _flags, _options) => {
901
911
  log(HELP);
902
912
  },
903
913
 
904
- 'changelog': async (args, apiInstance, flags, options) => {
914
+ 'changelog': async (_args, _apiInstance, _flags, _options) => {
905
915
  const changelogPath = new URL('../CHANGELOG.md', import.meta.url).pathname;
906
916
  let content;
907
917
  try {
@@ -910,7 +920,7 @@ export function buildCommands(deps = {}) {
910
920
  log('CHANGELOG.md not found. Visit https://github.com/nansen-ai/nansen-cli/blob/main/CHANGELOG.md');
911
921
  return;
912
922
  }
913
- const since = options.since;
923
+ const since = _options.since;
914
924
  if (since) {
915
925
  // Show only entries from the given version onwards
916
926
  const lines = content.split('\n');
@@ -936,8 +946,7 @@ export function buildCommands(deps = {}) {
936
946
  }
937
947
  },
938
948
 
939
- 'schema': async (args, apiInstance, flags, options) => {
940
- // Return schema for agent introspection
949
+ 'schema': async (args, _apiInstance, flags, _options) => {
941
950
  const subcommand = args[0];
942
951
  const schemaEntry = subcommand && (SCHEMA.commands[subcommand] || SCHEMA.commands.research.subcommands[subcommand]);
943
952
 
@@ -951,11 +960,14 @@ export function buildCommands(deps = {}) {
951
960
  };
952
961
  }
953
962
 
954
- // Return full schema
955
- return SCHEMA;
963
+ if (flags.full) {
964
+ return SCHEMA;
965
+ }
966
+
967
+ return compactSchema(SCHEMA);
956
968
  },
957
969
 
958
- 'cache': async (args, apiInstance, flags, options) => {
970
+ 'cache': async (args, _apiInstance, _flags, _options) => {
959
971
  const subcommand = args[0] || 'help';
960
972
 
961
973
  const handlers = {
@@ -1139,7 +1151,7 @@ export function buildCommands(deps = {}) {
1139
1151
 
1140
1152
  const handlers = {
1141
1153
  'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
1142
- 'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d', pagination }),
1154
+ 'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d' }),
1143
1155
  'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
1144
1156
  'screener': async () => {
1145
1157
  const search = options.search;
@@ -1299,7 +1311,7 @@ export function buildCommands(deps = {}) {
1299
1311
  }
1300
1312
  const category = RESEARCH_CATEGORY_ALIASES[rawCategory] || rawCategory;
1301
1313
  if (!RESEARCH_CATEGORIES.has(category)) {
1302
- return { error: `Unknown research category: ${rawCategory}`, available: [...RESEARCH_CATEGORIES] };
1314
+ throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES].join(', ')}`, ErrorCode.UNKNOWN);
1303
1315
  }
1304
1316
  return cmds[category](args.slice(1), apiInstance, flags, options);
1305
1317
  };
@@ -1335,10 +1347,7 @@ SYMBOLS:
1335
1347
  return;
1336
1348
  }
1337
1349
  if (!tradingCmds[sub]) {
1338
- log(`Unknown trade subcommand: ${sub}`);
1339
- log(`Available: quote, execute`);
1340
- log(`Run 'nansen trade help' for usage.`);
1341
- return;
1350
+ throw new NansenError(`Unknown trade subcommand: ${sub}. Available: quote, execute`, ErrorCode.UNKNOWN);
1342
1351
  }
1343
1352
  return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
1344
1353
  };
@@ -1351,9 +1360,6 @@ export const DEPRECATED_TO_RESEARCH = new Set(['smart-money', 'profiler', 'token
1351
1360
  // Subcommands that moved under 'trade'
1352
1361
  export const DEPRECATED_TO_TRADE = new Set(['quote', 'execute']);
1353
1362
 
1354
- // Commands that don't require API authentication
1355
- export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'trade', 'quote', 'execute', 'changelog'];
1356
-
1357
1363
  // Command aliases: top-level shortcuts that resolve before routing
1358
1364
  export const COMMAND_ALIASES = {
1359
1365
  'tgm': 'token', // Token God Mode
@@ -1372,7 +1378,6 @@ export const RESEARCH_CATEGORY_ALIASES = {
1372
1378
 
1373
1379
  // Generate help text for a specific subcommand using SCHEMA
1374
1380
  export function generateSubcommandHelp(command, subcommand) {
1375
- // Look up in top-level commands, then fall back to research subcommands
1376
1381
  const cmdSchema = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
1377
1382
  if (!cmdSchema) return null;
1378
1383
 
@@ -1380,100 +1385,36 @@ export function generateSubcommandHelp(command, subcommand) {
1380
1385
  if (!subSchema) return null;
1381
1386
 
1382
1387
  const lines = [];
1383
- lines.push(`\n${command} ${subcommand} - ${subSchema.description || 'No description'}\n`);
1384
-
1385
- // Usage
1386
- const requiredOpts = [];
1387
- const optionalOpts = [];
1388
-
1388
+ lines.push(`${command} ${subcommand} ${subSchema.description || 'No description'}`);
1389
+
1389
1390
  if (subSchema.options) {
1390
- for (const [name, opt] of Object.entries(subSchema.options)) {
1391
- if (opt.required) {
1392
- requiredOpts.push(name);
1393
- } else {
1394
- optionalOpts.push(name);
1395
- }
1396
- }
1397
- }
1398
-
1399
- let usage = `USAGE:\n nansen ${command} ${subcommand}`;
1400
- if (requiredOpts.length) {
1401
- usage += ' ' + requiredOpts.map(o => `--${o} <value>`).join(' ');
1402
- }
1403
- if (optionalOpts.length) {
1404
- usage += ' [options]';
1405
- }
1406
- lines.push(usage);
1407
-
1408
- // Required options
1409
- if (requiredOpts.length) {
1410
- lines.push('\nREQUIRED:');
1411
- for (const name of requiredOpts) {
1412
- const opt = subSchema.options[name];
1413
- const desc = opt.description || `${opt.type}`;
1414
- lines.push(` --${name.padEnd(16)} ${desc}`);
1415
- }
1416
- }
1417
-
1418
- // Optional options
1419
- if (optionalOpts.length) {
1420
- lines.push('\nOPTIONS:');
1421
- for (const name of optionalOpts) {
1422
- const opt = subSchema.options[name];
1423
- const defaultStr = opt.default !== undefined ? ` (default: ${opt.default})` : '';
1424
- const desc = (opt.description || opt.type) + defaultStr;
1425
- lines.push(` --${name.padEnd(16)} ${desc}`);
1426
- }
1391
+ const params = Object.entries(subSchema.options).map(([name, opt]) => {
1392
+ const parts = [`--${name}`];
1393
+ if (opt.required) parts[0] += '*';
1394
+ if (opt.default !== undefined) parts.push(`(${opt.default})`);
1395
+ if (opt.enum) parts.push(`[${opt.enum.join('|')}]`);
1396
+ return parts.join(' ');
1397
+ });
1398
+ lines.push(`Params (* required): ${params.join(', ')}`);
1427
1399
  }
1428
-
1429
- // Return fields
1430
- if (subSchema.returns && subSchema.returns.length) {
1431
- lines.push('\nRETURNS:');
1432
- lines.push(` ${subSchema.returns.join(', ')}`);
1400
+
1401
+ if (subSchema.returns?.length) {
1402
+ lines.push(`Returns: ${subSchema.returns.join(', ')}`);
1433
1403
  }
1434
-
1435
- // Examples
1436
- lines.push('\nEXAMPLES:');
1404
+
1405
+ const exampleValues = { address: '0x...', token: '0x...', query: '"term"', symbol: 'BTC', date: '2024-01-01' };
1437
1406
  const chain = subSchema.options?.chain?.default || 'solana';
1438
-
1439
- // Example values for common required options
1440
- const exampleValues = {
1441
- address: '0x123...',
1442
- token: '0x123...',
1443
- query: '"search term"',
1444
- symbol: 'BTC',
1445
- date: '2024-01-01'
1446
- };
1447
-
1448
- // Build example based on required options
1449
- let example = ` nansen ${command} ${subcommand}`;
1450
- for (const name of requiredOpts) {
1451
- const value = exampleValues[name] || '<value>';
1452
- example += ` --${name} ${value}`;
1407
+ let example = `nansen ${command} ${subcommand}`;
1408
+ if (subSchema.options) {
1409
+ for (const [name, opt] of Object.entries(subSchema.options)) {
1410
+ if (opt.required) example += ` --${name} ${exampleValues[name] || '<val>'}`;
1411
+ }
1453
1412
  }
1454
- if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
1413
+ if (subSchema.options?.chain && !subSchema.options.chain.required) {
1455
1414
  example += ` --chain ${chain}`;
1456
1415
  }
1457
- example += ' --pretty';
1458
- lines.push(example);
1459
-
1460
- // Add a filtered example if filters are supported
1461
- if (subSchema.options?.filters || subSchema.options?.labels) {
1462
- let filterExample = ` nansen ${command} ${subcommand}`;
1463
- for (const name of requiredOpts) {
1464
- const value = exampleValues[name] || '<value>';
1465
- filterExample += ` --${name} ${value}`;
1466
- }
1467
- if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
1468
- filterExample += ` --chain ${chain}`;
1469
- }
1470
- if (subSchema.options?.labels) {
1471
- filterExample += ' --labels "Smart Trader"';
1472
- }
1473
- filterExample += ' --limit 10 --table';
1474
- lines.push(filterExample);
1475
- }
1476
-
1416
+ lines.push(`Example: ${example}`);
1417
+
1477
1418
  return lines.join('\n');
1478
1419
  }
1479
1420
 
@@ -1542,13 +1483,10 @@ export async function runCLI(rawArgs, deps = {}) {
1542
1483
  const researchCat = SCHEMA.commands.research.subcommands[category];
1543
1484
  if (researchCat) {
1544
1485
  const catSchema = researchCat;
1545
- const lines = [`\nresearch ${category} - ${catSchema.description}\n`];
1486
+ const lines = [`research ${category} ${catSchema.description}`];
1546
1487
  if (catSchema.subcommands) {
1547
- lines.push('SUBCOMMANDS:');
1548
- for (const [sub, subSchema] of Object.entries(catSchema.subcommands)) {
1549
- lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
1550
- }
1551
- lines.push(`\nFor detailed help: nansen research ${category} <subcommand> --help`);
1488
+ lines.push('Subcommands: ' + Object.keys(catSchema.subcommands).join(', '));
1489
+ lines.push(`Use: nansen research ${category} <subcommand> --help`);
1552
1490
  }
1553
1491
  output(lines.join('\n'));
1554
1492
  notify();
@@ -1577,13 +1515,10 @@ export async function runCLI(rawArgs, deps = {}) {
1577
1515
  const cmdSchemaLookup = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
1578
1516
  if (command && cmdSchemaLookup) {
1579
1517
  const cmdSchema = cmdSchemaLookup;
1580
- const lines = [`\n${command} - ${cmdSchema.description}\n`];
1518
+ const lines = [`${command} ${cmdSchema.description}`];
1581
1519
  if (cmdSchema.subcommands) {
1582
- lines.push('SUBCOMMANDS:');
1583
- for (const [sub, subSchema] of Object.entries(cmdSchema.subcommands)) {
1584
- lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
1585
- }
1586
- lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
1520
+ lines.push('Subcommands: ' + Object.keys(cmdSchema.subcommands).join(', '));
1521
+ lines.push(`Use: nansen ${command} <subcommand> --help`);
1587
1522
  }
1588
1523
  output(lines.join('\n'));
1589
1524
  notify();
@@ -1610,48 +1545,46 @@ export async function runCLI(rawArgs, deps = {}) {
1610
1545
  return { type: 'error', data: errorData };
1611
1546
  }
1612
1547
 
1613
- // Commands that don't require API authentication
1614
- if (NO_AUTH_COMMANDS.includes(command)) {
1615
- const result = await commands[command](subArgs, null, flags, options);
1616
-
1617
- // Schema command returns data that should be output
1618
- if (command === 'schema' && result) {
1619
- const formatted = formatOutput(result, { pretty, table: false });
1620
- output(formatted.text);
1621
- notify();
1622
- return { type: 'schema', data: result };
1623
- }
1624
-
1625
- notify();
1626
- return { type: 'no-auth', command };
1627
- }
1628
-
1629
1548
  try {
1630
1549
  // Configure retry options
1631
- const retryOptions = flags['no-retry']
1632
- ? { maxRetries: 0 }
1550
+ const retryOptions = flags['no-retry']
1551
+ ? { maxRetries: 0 }
1633
1552
  : { maxRetries: options.retries !== undefined ? (Number.isNaN(parseInt(options.retries, 10)) ? 3 : parseInt(options.retries, 10)) : 3 };
1634
-
1553
+
1635
1554
  // Configure cache options
1636
1555
  const cacheTtl = options['cache-ttl'] !== undefined ? parseInt(options['cache-ttl'], 10) : 300;
1637
1556
  const cacheOptions = {
1638
1557
  enabled: flags['cache'] && !flags['no-cache'],
1639
1558
  ttl: Number.isNaN(cacheTtl) ? 300 : cacheTtl
1640
1559
  };
1641
-
1560
+
1642
1561
  const defaultHeaders = {};
1643
1562
  if (options['x402-payment-signature']) {
1644
1563
  defaultHeaders['Payment-Signature'] = options['x402-payment-signature'];
1645
1564
  }
1646
1565
  const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions, defaultHeaders });
1647
1566
  let result = await commands[command](subArgs, api, flags, options);
1648
-
1567
+
1568
+ // Commands that handle their own output return undefined
1569
+ if (result === undefined) {
1570
+ notify();
1571
+ return { type: 'no-output', command };
1572
+ }
1573
+
1574
+ // Schema returns data directly (not wrapped in { success, data })
1575
+ if (command === 'schema') {
1576
+ const formatted = formatOutput(result, { pretty, table: false });
1577
+ output(formatted.text);
1578
+ notify();
1579
+ return { type: 'schema', data: result };
1580
+ }
1581
+
1649
1582
  // Apply field filtering if --fields is specified
1650
1583
  const fields = parseFields(options.fields);
1651
1584
  if (fields) {
1652
1585
  result = filterFields(result, fields);
1653
1586
  }
1654
-
1587
+
1655
1588
  // Output in requested format
1656
1589
  if (stream) {
1657
1590
  // Stream mode: output each record as a JSON line (NDJSON)
package/src/trading.js CHANGED
@@ -742,7 +742,7 @@ export function validateBaseUnitAmount(amount) {
742
742
  return null;
743
743
  }
744
744
 
745
- function formatQuote(quote, index) {
745
+ export function formatQuote(quote, index) {
746
746
  const lines = [];
747
747
  const label = index !== undefined ? ` Quote #${index + 1}` : ' Best Quote';
748
748
  lines.push(`${label} (${quote.aggregator || 'unknown'})`);
@@ -750,10 +750,21 @@ function formatQuote(quote, index) {
750
750
  lines.push(` Output: ${quote.outAmount} → ${quote.outputMint?.slice(0, 12)}...`);
751
751
  if (quote.inUsdValue) lines.push(` In USD: $${quote.inUsdValue}`);
752
752
  if (quote.outUsdValue) lines.push(` Out USD: $${quote.outUsdValue}`);
753
- if (quote.priceImpactPct) lines.push(` Price Impact: ${quote.priceImpactPct}%`);
753
+ if (quote.priceImpactPct) {
754
+ const impactAbs = Math.abs(parseFloat(quote.priceImpactPct));
755
+ if (impactAbs <= 5) {
756
+ lines.push(` Price Impact: ${impactAbs}%`);
757
+ }
758
+ }
754
759
  if (quote.tradingFeeInUsd) lines.push(` Trading Fee: $${quote.tradingFeeInUsd}`);
755
760
  if (quote.networkFeeInUsd) lines.push(` Network Fee: $${quote.networkFeeInUsd}`);
756
761
  if (quote.approvalAddress && !isNativeToken(quote.inputMint)) lines.push(` ⚠ Requires token approval to: ${quote.approvalAddress}`);
762
+ if (quote.priceImpactPct) {
763
+ const impactAbs = Math.abs(parseFloat(quote.priceImpactPct));
764
+ if (impactAbs > 5) {
765
+ lines.push(` ⚠ Price impact is ${impactAbs}%! You may lose significant value.`);
766
+ }
767
+ }
757
768
  return lines.join('\n');
758
769
  }
759
770
 
@@ -833,11 +844,15 @@ EXAMPLES:
833
844
  const wallet = showWallet(walletName);
834
845
  walletAddress = chainType === 'solana' ? wallet.solana : wallet.evm;
835
846
  } else {
836
- walletAddress = getDefaultAddress(chainType);
847
+ try {
848
+ walletAddress = getDefaultAddress(chainType);
849
+ } catch {
850
+ // No wallet configured — fall through to the check below
851
+ }
837
852
  }
838
853
 
839
854
  if (!walletAddress) {
840
- errorOutput('No wallet found. Create one with: nansen wallet create');
855
+ errorOutput('No wallet found. A wallet address is required for quotes because the trading API builds a transaction specific to the sender.\nCreate one with: nansen wallet create');
841
856
  exit(1);
842
857
  return;
843
858
  }
@@ -877,6 +892,9 @@ EXAMPLES:
877
892
  const quoteId = saveQuote(response, chain, isWalletConnect ? 'walletconnect' : 'local');
878
893
  errorOutput(`\n Quote ID: ${quoteId}`);
879
894
  errorOutput(` Execute: nansen trade execute --quote ${quoteId}`);
895
+ if (response.quotes.length > 1) {
896
+ errorOutput(` Pin #1: nansen trade execute --quote ${quoteId} --quote-index 0`);
897
+ }
880
898
 
881
899
  if (response.quotes[0]?.approvalAddress && !isNativeToken(response.quotes[0]?.inputMint)) {
882
900
  errorOutput(`\n Warning: This token swap requires an ERC-20 approval step.`);
package/src/transfer.js CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import crypto from 'crypto';
8
8
  import { base58Encode, exportWallet, getWalletConfig, verifyPassword } from './wallet.js';
9
- import { keccak256, signSecp256k1, rlpEncode, bigIntToMinBuf } from './crypto.js';
9
+ import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
10
10
  import { getWalletConnectAddress, sendTransactionViaWalletConnect } from './walletconnect-trading.js';
11
11
  import { EVM_CHAIN_IDS } from './chain-ids.js';
12
12
 
@@ -217,7 +217,6 @@ async function buildEvmTransaction({ to, amount, token, privateKey, chain, max =
217
217
  const safeReserve = l2GasCost * 3n;
218
218
  if (ethBalance <= safeReserve) throw new Error(`Insufficient balance: ${ethBalance} wei (need > ${safeReserve} for gas + L1 fees)`);
219
219
  txValue = ethBalance - safeReserve;
220
- amount = txValue;
221
220
  stderr(` Max send: ${formatAmount(txValue, 18)} ETH (reserved ${formatAmount(safeReserve, 18)} for gas)`);
222
221
  } else {
223
222
  txValue = amount;
@@ -375,7 +374,7 @@ async function buildSolanaTransaction({ to, amount, amountStr, token, privateKey
375
374
  }
376
375
  } catch (e) {
377
376
  if (e.message.includes('Insufficient')) throw e;
378
- throw new Error(`Source token account not found. Do you hold this token?`);
377
+ throw new Error(`Source token account not found. Do you hold this token?`, { cause: e });
379
378
  }
380
379
 
381
380
  // TransferChecked instruction data: [12, amount u64 LE, decimals u8]
@@ -568,7 +567,7 @@ async function validateErc20Token(rpcUrl, tokenAddress) {
568
567
  return decimals;
569
568
  } catch (e) {
570
569
  if (e.message.includes('not a valid')) throw e;
571
- throw new Error(`Contract ${tokenAddress} does not implement ERC-20 decimals() — may not be a valid token`);
570
+ throw new Error(`Contract ${tokenAddress} does not implement ERC-20 decimals() — may not be a valid token`, { cause: e });
572
571
  }
573
572
  }
574
573
 
@@ -626,7 +625,7 @@ export async function sendTokens({ to, amount, chain, token = null, wallet = nul
626
625
  } else if (max && token) {
627
626
  // Max SPL: full token balance
628
627
  const rpcUrl = CHAIN_RPCS.solana;
629
- const { tokenProgram, decimals } = await getTokenInfo(rpcUrl, token);
628
+ const { tokenProgram, decimals: _decimals } = await getTokenInfo(rpcUrl, token);
630
629
  const sourceATA = deriveATA(walletData.solana.address, token, tokenProgram);
631
630
  const sourceAtaAddr = base58Encode(sourceATA);
632
631
  const ataInfo = await rpcCall(rpcUrl, 'getTokenAccountBalance', [sourceAtaAddr]);
@@ -721,7 +720,7 @@ async function sendTokensViaWalletConnect({ to, amount, chain, token, max, dryRu
721
720
  const wcAddress = await getWalletConnectAddress();
722
721
  if (!wcAddress) throw new Error('No WalletConnect session active. Run: walletconnect connect');
723
722
 
724
- let txTo, txValue, txData, decimals = 18;
723
+ let txTo, txValue, txData;
725
724
 
726
725
  if (token) {
727
726
  // Validate ERC-20 contract
@@ -730,7 +729,7 @@ async function sendTokensViaWalletConnect({ to, amount, chain, token, max, dryRu
730
729
  throw new Error(`Address ${token} is not a contract — not a valid ERC-20 token`);
731
730
  }
732
731
  const decResult = await rpcCall(rpcUrl, 'eth_call', [{ to: token, data: '0x313ce567' }, 'latest']);
733
- decimals = parseInt(decResult, 16);
732
+ const decimals = parseInt(decResult, 16);
734
733
 
735
734
  if (max) {
736
735
  // Max ERC-20: full token balance
@@ -809,14 +808,11 @@ async function sendTokensViaWalletConnect({ to, amount, chain, token, max, dryRu
809
808
  chainId,
810
809
  });
811
810
 
812
- let txHash;
813
- if (wcResult.txHash) {
814
- txHash = wcResult.txHash;
815
- } else if (wcResult.signedTransaction) {
816
- txHash = await broadcastTransaction(wcResult.signedTransaction, chain);
817
- } else {
811
+ const txHash = await (async () => {
812
+ if (wcResult.txHash) return wcResult.txHash;
813
+ if (wcResult.signedTransaction) return broadcastTransaction(wcResult.signedTransaction, chain);
818
814
  throw new Error('No transaction hash or signed transaction returned from WalletConnect');
819
- }
815
+ })();
820
816
 
821
817
  // Wait for confirmation
822
818
  const confirmation = await waitForEvmConfirmation(rpcUrl, txHash);
package/src/wallet.js CHANGED
@@ -26,7 +26,7 @@ const SCRYPT_P = 1;
26
26
  const SCRYPT_KEYLEN = 32;
27
27
  const SALT_LEN = 16;
28
28
  const IV_LEN = 12;
29
- const AUTH_TAG_LEN = 16;
29
+ const _AUTH_TAG_LEN = 16;
30
30
 
31
31
  import { keccak256 } from './crypto.js';
32
32
 
@@ -481,7 +481,7 @@ export function getDefaultAddress(chainType = 'evm') {
481
481
  * Build wallet command handlers for integration into CLI.
482
482
  */
483
483
  export function buildWalletCommands(deps = {}) {
484
- const { log = console.log, promptFn, exit = process.exit } = deps;
484
+ const { log = console.log, promptFn: _promptFn, exit = process.exit } = deps;
485
485
 
486
486
  return {
487
487
  'wallet': async (args, apiInstance, flags, options) => {
@@ -11,7 +11,7 @@ import { execFile } from 'child_process';
11
11
  */
12
12
  export function wcExec(cmd, args, timeoutMs = 10000) {
13
13
  return new Promise((resolve, reject) => {
14
- execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, stderr) => {
14
+ execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, _stderr) => {
15
15
  if (err) {
16
16
  reject(new Error(err.message));
17
17
  return;
package/src/x402-svm.js CHANGED
@@ -30,11 +30,11 @@ export function base58Encode(buf) {
30
30
  // ============= Constants =============
31
31
 
32
32
  const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
33
- const TOKEN_2022_PROGRAM = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
33
+ const _TOKEN_2022_PROGRAM = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
34
34
  const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
35
35
  const MEMO_PROGRAM = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
36
36
  const ATA_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
37
- const SYSTEM_PROGRAM = '11111111111111111111111111111111';
37
+ const _SYSTEM_PROGRAM = '11111111111111111111111111111111';
38
38
 
39
39
  const DEFAULT_COMPUTE_UNIT_LIMIT = 20000;
40
40
  const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
@@ -186,7 +186,7 @@ function modInverse(a, mod) {
186
186
  * Build a Solana MessageV0 from accounts and instructions.
187
187
  * Simplified builder for x402 payment transactions.
188
188
  */
189
- function buildMessageV0({ feePayer, instructions, recentBlockhash, accounts }) {
189
+ function buildMessageV0({ feePayer, instructions, recentBlockhash, accounts: _accounts }) {
190
190
  // All unique accounts in order: feePayer first, then signers, then rest
191
191
  const accountMap = new Map();
192
192
  const feePayerKey = feePayer;
package/src/x402.js CHANGED
@@ -154,7 +154,7 @@ export async function createPaymentSignature(response, url, options = {}) {
154
154
  */
155
155
  export async function checkX402Balance(network) {
156
156
  try {
157
- const { listWallets, exportWallet } = await import('./wallet.js');
157
+ const { listWallets, exportWallet: _exportWallet } = await import('./wallet.js');
158
158
  const wallets = listWallets();
159
159
  if (!wallets.defaultWallet) return null;
160
160