nansen-cli 1.22.0 → 1.23.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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.23.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#341](https://github.com/nansen-ai/nansen-cli/pull/341) [`4b60056`](https://github.com/nansen-ai/nansen-cli/commit/4b6005697d52b5d432b9b32bcd1d36422f9166cc) Thanks [@gulshngill](https://github.com/gulshngill)! - Add `--webhook <url>` and `--webhook-secret <secret>` flags to `alerts create` and `alerts update`.
8
+
9
+ Allows alerts to be delivered to any HTTP/HTTPS endpoint via POST, alongside
10
+ the existing `--telegram`, `--slack`, and `--discord` channels. The optional
11
+ `--webhook-secret` enables HMAC payload signing for verification.
12
+
13
+ ### Patch Changes
14
+
15
+ - [#344](https://github.com/nansen-ai/nansen-cli/pull/344) [`3dc09cc`](https://github.com/nansen-ai/nansen-cli/commit/3dc09cc8aa38cd4da4ae305f83e9599efb3b9ff9) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Add nansen-agent-guide skill — routing guide for when to use `nansen agent` vs direct CLI data commands
16
+
17
+ - [#347](https://github.com/nansen-ai/nansen-cli/pull/347) [`a243c7a`](https://github.com/nansen-ai/nansen-cli/commit/a243c7a33f28057949d2060c732083642422eb18) Thanks [@kome12](https://github.com/kome12)! - Add --buy-or-sell option to `token who-bought-sold` command — allows filtering by buy or sell side (BUY | SELL, defaults to BUY)
18
+
3
19
  ## 1.22.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -27,6 +27,8 @@ Get your API key at [app.nansen.ai/auth/agent-setup](https://app.nansen.ai/auth/
27
27
 
28
28
  ```
29
29
  nansen research <category> <subcommand> [options]
30
+ nansen agent "<question>" # AI research agent (200 credits, Pro)
31
+ nansen agent "<question>" --expert # deeper analysis (750 credits, Pro)
30
32
  nansen trade <subcommand> [options]
31
33
  nansen wallet <subcommand> [options]
32
34
  nansen schema [command] [--pretty] # full command reference (no API key needed)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: nansen-agent-guide
3
+ description: Routing guide -- when to use `nansen agent` (AI research) vs direct CLI data commands. Use when deciding how to answer a user's research question with Nansen tools.
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
+ # Agent vs CLI Routing
20
+
21
+ | Need a... | Use |
22
+ |-----------|-----|
23
+ | **take** (analysis, interpretation) | `nansen agent` |
24
+ | **table** (raw data, specific metrics) | Direct CLI commands |
25
+ | **report** (both) | Agent for narrative + CLI for data |
26
+
27
+ ## Use `nansen agent` when
28
+
29
+ - Question requires interpretation or synthesis across multiple data sources
30
+ - Open-ended research: "analyse this wallet", "what's happening with ETH smart money?"
31
+
32
+ ```bash
33
+ nansen agent "What are top smart money tokens on Solana today and why?"
34
+ nansen agent "Analyse wallet 0x123... -- is this a smart trader?"
35
+ nansen agent "..." --expert # deeper analysis, 750 credits
36
+ ```
37
+
38
+ Cost: 200 credits (fast) / 750 credits (expert)
39
+
40
+ ## Use direct CLI commands when
41
+
42
+ - You need specific structured data -- prices, volumes, holders, flows
43
+ - Deterministic question: "top 10 tokens by netflow on ethereum"
44
+ - Piping output or building a data table
45
+
46
+ ```bash
47
+ nansen research token screener --chain ethereum --smart-money --limit 10
48
+ nansen research smart-money netflow --chain solana
49
+ nansen research profiler balance --address 0x123... --chain ethereum
50
+ ```
51
+
52
+ Cost: 5-50 credits per call
53
+
54
+ ## Anti-patterns
55
+
56
+ - Don't use `nansen agent` for simple data fetches -- 40x more expensive
57
+ - Don't use raw CLI for open-ended analysis -- returns data, not interpretation
58
+ - Don't chain 3+ agent calls -- get raw data via CLI, call agent once for synthesis
@@ -41,6 +41,8 @@ nansen alerts delete <id>
41
41
  | `--telegram` | chat ID | optional | | |
42
42
  | `--slack` | webhook URL | optional | | |
43
43
  | `--discord` | webhook URL | optional | | |
44
+ | `--webhook` | endpoint URL | optional | optional | |
45
+ | `--webhook-secret` | optional (webhook only) | optional | | |
44
46
  | `--description` | optional | optional | | |
45
47
  | `--enabled` | | flag | flag | |
46
48
  | `--disabled` | flag | flag | flag | |
@@ -131,7 +133,8 @@ nansen alerts create \
131
133
  ## Notes
132
134
 
133
135
  - Chain aliases: Hyperliquid = `hyperevm`, BSC = `bnb`.
134
- - Multiple channels can be combined: `--telegram 123 --slack https://...`
136
+ - Multiple channels can be combined: `--telegram 123 --slack https://... --webhook https://...`
137
+ - `--webhook <url>` sends a POST request with the alert payload to any HTTP/HTTPS endpoint. Useful for server deployments, Zapier, n8n, or custom integrations. The endpoint must be publicly reachable and return a 2xx response.
135
138
  - `--data '<json>'` merges raw JSON on top of named flags (escape hatch for fields without named flags).
136
139
  - Alert endpoints are internal-only. Non-internal users receive 404.
137
140
  - Use single quotes for names with `$` or special characters: `--name 'SM >$1M'`
package/src/api.js CHANGED
@@ -293,6 +293,19 @@ export function validateAddress(address, chain = 'ethereum') {
293
293
  return { valid: true };
294
294
  }
295
295
 
296
+ /**
297
+ * Normalize EVM address to lowercase for API compatibility.
298
+ * The API should handle case-insensitive addresses server-side, but this is
299
+ * a defensive client-side measure since checksummed addresses currently
300
+ * return empty results.
301
+ */
302
+ export function normalizeAddress(address, chain = 'ethereum') {
303
+ if (address && typeof address === 'string' && address.startsWith('0x') && EVM_CHAINS.includes(chain)) {
304
+ return address.toLowerCase();
305
+ }
306
+ return address;
307
+ }
308
+
296
309
  /**
297
310
  * Validate token address (same rules as wallet address)
298
311
  */
@@ -1030,7 +1043,7 @@ export class NansenAPI {
1030
1043
  }
1031
1044
 
1032
1045
  async tokenWhoBoughtSold(params = {}) {
1033
- const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30, date } = params;
1046
+ const { tokenAddress, chain = 'solana', buyOrSell = 'BUY', filters = {}, orderBy, pagination, days = 30, date } = params;
1034
1047
  if (tokenAddress) {
1035
1048
  const validation = validateTokenAddress(tokenAddress, chain);
1036
1049
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
@@ -1039,6 +1052,7 @@ export class NansenAPI {
1039
1052
  return this.request('/api/v1/tgm/who-bought-sold', {
1040
1053
  token_address: tokenAddress,
1041
1054
  chain,
1055
+ buy_or_sell: buyOrSell,
1042
1056
  date: dateRange,
1043
1057
  filters,
1044
1058
  order_by: orderBy,
package/src/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Extracted from index.js for coverage
4
4
  */
5
5
 
6
- import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, sleep } from './api.js';
6
+ import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, normalizeAddress, sleep } from './api.js';
7
7
  import { buildWalletCommands } from './wallet.js';
8
8
  import { buildTradingCommands } from './trading.js';
9
9
  import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
@@ -870,7 +870,7 @@ export function buildCommands(deps = {}) {
870
870
  return;
871
871
  }
872
872
 
873
- let apiKey = options['api-key'] || options.apiKey;
873
+ let apiKey = options['api-key'];
874
874
 
875
875
  if (!apiKey) {
876
876
  apiKey = process.env.NANSEN_API_KEY;
@@ -1187,9 +1187,9 @@ export function buildCommands(deps = {}) {
1187
1187
 
1188
1188
  'token': async (args, apiInstance, flags, options) => {
1189
1189
  const subcommand = args[0] || 'help';
1190
- const tokenAddress = options.token || options['token-address'];
1191
- const tokenSymbol = options.symbol || options['token-symbol'];
1192
1190
  const chain = options.chain || 'solana';
1191
+ const tokenAddress = normalizeAddress(options.token || options['token-address'], chain);
1192
+ const tokenSymbol = options.symbol || options['token-symbol'];
1193
1193
  const chains = options.chains || [chain];
1194
1194
  const timeframe = options.timeframe || '24h';
1195
1195
  const filters = options.filters || {};
@@ -1247,7 +1247,8 @@ export function buildCommands(deps = {}) {
1247
1247
  'pnl': () => apiInstance.tokenPnlLeaderboard({ tokenAddress, chain, filters, orderBy, pagination, days }),
1248
1248
  'who-bought-sold': () => {
1249
1249
  const date = parseDateOption(options.date, days);
1250
- return apiInstance.tokenWhoBoughtSold({ tokenAddress, chain, filters, orderBy, pagination, days, date });
1250
+ const buyOrSell = (options['buy-or-sell'] || 'BUY').toUpperCase();
1251
+ return apiInstance.tokenWhoBoughtSold({ tokenAddress, chain, buyOrSell, filters, orderBy, pagination, days, date });
1251
1252
  },
1252
1253
  'flow-intelligence': () => apiInstance.tokenFlowIntelligence({ tokenAddress, chain, days }),
1253
1254
  'transfers': () => {
@@ -484,10 +484,11 @@ USAGE:
484
484
  REQUIRED:
485
485
  --name <name> Alert name
486
486
  --type <type> sm-token-flows | common-token-transfer | smart-contract-call
487
- At least one channel: --telegram <chatId> | --slack <url> | --discord <url>
487
+ At least one channel: --telegram <chatId> | --slack <url> | --discord <url> | --webhook <url>
488
488
 
489
489
  OPTIONS (all types):
490
490
  --chains <chains> Comma-separated chains (e.g. ethereum,solana)
491
+ --webhook-secret <secret> Signing secret for webhook payload verification (webhook only)
491
492
  --token <address:chain> Include token (repeatable)
492
493
  --exclude-token <addr:chain> Exclude token (repeatable)
493
494
  --description '<text>' Alert description
@@ -565,6 +566,14 @@ USAGE:
565
566
  if (options.telegram) channels.push({ type: 'telegram', data: { chatId: String(options.telegram) } });
566
567
  if (options.slack) channels.push({ type: 'slack', data: { webhookUrl: options.slack } });
567
568
  if (options.discord) channels.push({ type: 'discord', data: { webhookUrl: options.discord } });
569
+ if (options.webhook) {
570
+ const webhookData = { webhookUrl: options.webhook };
571
+ if (options["webhook-secret"]) {
572
+ if (options["webhook-secret"].length < 16) throw new NansenError('--webhook-secret must be at least 16 characters', ErrorCode.INVALID_PARAMS);
573
+ webhookData.secret = options["webhook-secret"];
574
+ }
575
+ channels.push({ type: 'webhook', data: webhookData });
576
+ }
568
577
  return channels.length > 0 ? channels : null;
569
578
  }
570
579
 
@@ -608,7 +617,7 @@ USAGE:
608
617
  if (!name) missing.push('--name');
609
618
  if (!type) missing.push('--type');
610
619
  if (!options.chains) missing.push('--chains');
611
- if (!channels) missing.push('a channel (--telegram, --slack, or --discord)');
620
+ if (!channels) missing.push('a channel (--telegram, --slack, --discord, or --webhook)');
612
621
  if (missing.length > 0) {
613
622
  throw new NansenError(`Required: ${missing.join(', ')}`, ErrorCode.MISSING_PARAM);
614
623
  }
@@ -707,7 +716,9 @@ USAGE:
707
716
  ? `Invalid Slack webhook URL. Check the URL and try again.`
708
717
  : ch?.type === 'discord'
709
718
  ? `Invalid Discord webhook URL. Check the URL and try again.`
710
- : err.message;
719
+ : ch?.type === 'webhook'
720
+ ? `Invalid webhook URL (${ch.data.webhookUrl}). Ensure the endpoint is reachable and returns 2xx.`
721
+ : err.message;
711
722
  throw new NansenError(hint, err.code ?? ErrorCode.INVALID_PARAMS, err.status);
712
723
  }
713
724
  throw err;
package/src/schema.json CHANGED
@@ -300,6 +300,11 @@
300
300
  },
301
301
  "days": {
302
302
  "default": 30
303
+ },
304
+ "buy-or-sell": {
305
+ "description": "Filter by buy or sell side",
306
+ "enum": ["BUY", "SELL"],
307
+ "default": "BUY"
303
308
  }
304
309
  }
305
310
  },
@@ -669,6 +674,14 @@
669
674
  "type": "string",
670
675
  "description": "Discord webhook URL for notifications"
671
676
  },
677
+ "webhook": {
678
+ "type": "string",
679
+ "description": "HTTP/HTTPS endpoint URL to POST alert payloads to"
680
+ },
681
+ "webhook-secret": {
682
+ "type": "string",
683
+ "description": "Signing secret for webhook payload verification (optional, webhook only)"
684
+ },
672
685
  "data": {
673
686
  "type": "string",
674
687
  "description": "Alert config JSON. --chains is merged on top."
@@ -710,6 +723,14 @@
710
723
  "type": "string",
711
724
  "description": "Discord webhook URL"
712
725
  },
726
+ "webhook": {
727
+ "type": "string",
728
+ "description": "HTTP/HTTPS endpoint URL to POST alert payloads to"
729
+ },
730
+ "webhook-secret": {
731
+ "type": "string",
732
+ "description": "Signing secret for webhook payload verification (optional, webhook only)"
733
+ },
713
734
  "data": {
714
735
  "type": "string",
715
736
  "description": "Alert config JSON. --chains merged on top."
package/src/trading.js CHANGED
@@ -115,9 +115,6 @@ export async function getQuote(params) {
115
115
  }
116
116
 
117
117
  const headers = { 'Accept': 'application/json' };
118
- if (process.env.NANSEN_API_KEY) {
119
- headers['Authorization'] = `Bearer ${process.env.NANSEN_API_KEY}`;
120
- }
121
118
 
122
119
  const res = await fetch(url.toString(), { headers });
123
120
 
@@ -156,10 +153,6 @@ export async function executeTransaction(params, { retries = 2, retryDelayMs = 1
156
153
  'Content-Type': 'application/json',
157
154
  'Accept': 'application/json',
158
155
  };
159
- if (process.env.NANSEN_API_KEY) {
160
- headers['Authorization'] = `Bearer ${process.env.NANSEN_API_KEY}`;
161
- }
162
-
163
156
  let lastError;
164
157
  for (let attempt = 0; attempt <= retries; attempt++) {
165
158
  if (attempt > 0) {
@@ -841,7 +834,7 @@ export function buildTradingCommands(deps = {}) {
841
834
  const amount = options.amount || args[3];
842
835
  const walletName = options.wallet;
843
836
  const slippage = options.slippage;
844
- const autoSlippage = flags['auto-slippage'] || flags.autoSlippage;
837
+ const autoSlippage = flags['auto-slippage'];
845
838
  const maxAutoSlippage = options['max-auto-slippage'];
846
839
  const swapMode = options['swap-mode'] || 'exactIn';
847
840
  const amountUnit = options['amount-unit'];
@@ -1013,7 +1006,7 @@ EXAMPLES:
1013
1006
  'execute': async (args, apiInstance, flags, options) => {
1014
1007
  const quoteId = options.quote || options['quote-id'] || args[0];
1015
1008
  const walletName = options.wallet;
1016
- const noSimulate = flags['no-simulate'] || flags.noSimulate;
1009
+ const noSimulate = flags['no-simulate'];
1017
1010
 
1018
1011
  if (!quoteId) {
1019
1012
  log(`
package/src/wallet.js CHANGED
@@ -907,7 +907,7 @@ export function buildWalletCommands(deps = {}) {
907
907
  }
908
908
  password = resolved.password;
909
909
  }
910
- const dryRun = flags['dry-run'] || flags.dryRun;
910
+ const dryRun = flags['dry-run'];
911
911
 
912
912
  try {
913
913
  const sendOpts = {