nansen-cli 1.11.2 → 1.13.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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#107](https://github.com/nansen-ai/nansen-cli/pull/107) [`5877c06`](https://github.com/nansen-ai/nansen-cli/commit/5877c06061c1d32998fa3ce011c16f4352fc22dc) Thanks [@marius-reed](https://github.com/marius-reed)! - Add 11 prediction market (Polymarket) endpoints under `nansen research pm`. Includes OHLCV, orderbook, top holders, trades, screeners, PnL, position detail, and categories. Supports `--market-id`, `--address`, `--sort-by`, `--query`, `--status` flags with pagination, sorting, and table output.
8
+
9
+ ## 1.12.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#207](https://github.com/nansen-ai/nansen-cli/pull/207) [`73ca500`](https://github.com/nansen-ai/nansen-cli/commit/73ca5009c03ad541673165ca6b50f33ff4cc1673) Thanks [@TimNooren](https://github.com/TimNooren)! - Add --unsafe-no-password flag to wallet create for agent-friendly passwordless wallets.
14
+
15
+ ### Patch Changes
16
+
17
+ - [#212](https://github.com/nansen-ai/nansen-cli/pull/212) [`726c29d`](https://github.com/nansen-ai/nansen-cli/commit/726c29d2676c8a37772299c6237b44890493dfa5) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Clarify empty input handling in parseAddressList with explicit early return
18
+
19
+ - [#218](https://github.com/nansen-ai/nansen-cli/pull/218) [`8c4dd71`](https://github.com/nansen-ai/nansen-cli/commit/8c4dd71ce215026e149a6b097540c46622f13d3a) Thanks [@TimNooren](https://github.com/TimNooren)! - fix: `nansen changelog --since <version>` now correctly filters changeset-format entries (## x.y.z) in addition to Keep a Changelog entries (## [x.y.z])
20
+
21
+ - [#209](https://github.com/nansen-ai/nansen-cli/pull/209) [`a6dc1ed`](https://github.com/nansen-ai/nansen-cli/commit/a6dc1ed9dc40ad3506e7debe09746d463a70c14d) Thanks [@0xlaveen](https://github.com/0xlaveen)! - fix: prevent --help from executing destructive commands (logout, schema, cache)
22
+
3
23
  ## 1.11.2
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -30,7 +30,7 @@ nansen wallet <subcommand> [options]
30
30
  nansen schema [command] [--pretty] # full command reference (no API key needed)
31
31
  ```
32
32
 
33
- **Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `search`, `perp`, `points`
33
+ **Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `prediction-market` (`pm`), `search`, `perp`, `points`
34
34
 
35
35
  **Trade:** `quote`, `execute` — DEX swaps on Solana and Base.
36
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.11.2",
3
+ "version": "1.13.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "lint": "eslint .",
25
25
  "lint:fix": "eslint . --fix",
26
26
  "changeset": "changeset",
27
- "changeset:version": "changeset version",
27
+ "changeset:version": "changeset version && npm install --package-lock-only",
28
28
  "changeset:publish": "changeset publish"
29
29
  },
30
30
  "keywords": [
package/src/api.js CHANGED
@@ -582,7 +582,9 @@ export class NansenAPI {
582
582
  // No API key and no payment wallet — guide the user to login rather than
583
583
  // showing a confusing x402 payment dump they can't act on.
584
584
  // TODO: full fix would skip x402 entirely when no apiKey is set — see PR #<this PR number>
585
- message = 'No API key configured. Run: nansen login --api-key <key>. Get your key at https://app.nansen.ai/api';
585
+ message = 'No API key configured. Two ways to authenticate:\n' +
586
+ ' 1. API key: nansen login --api-key <key> (get key at https://app.nansen.ai/api)\n' +
587
+ ' 2. x402 micropayment: nansen wallet create + fund with USDC (no API key needed)';
586
588
  } else {
587
589
  message = `x402 auto-payment failed: ${x402Err.message}`;
588
590
  }
@@ -1101,6 +1103,113 @@ export class NansenAPI {
1101
1103
  });
1102
1104
  }
1103
1105
 
1106
+ // ============= Prediction Market Endpoints =============
1107
+
1108
+ async pmOhlcv(params = {}) {
1109
+ const { marketId, sort, pagination } = params;
1110
+ if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1111
+ return this.request('/api/v1/prediction-market/ohlcv', {
1112
+ market_id: marketId,
1113
+ sort,
1114
+ pagination
1115
+ });
1116
+ }
1117
+
1118
+ async pmOrderbook(params = {}) {
1119
+ const { marketId, pagination } = params;
1120
+ if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1121
+ return this.request('/api/v1/prediction-market/orderbook', {
1122
+ market_id: marketId,
1123
+ pagination
1124
+ });
1125
+ }
1126
+
1127
+ async pmTopHolders(params = {}) {
1128
+ const { marketId, sort, pagination } = params;
1129
+ if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1130
+ return this.request('/api/v1/prediction-market/top-holders', {
1131
+ market_id: marketId,
1132
+ sort,
1133
+ pagination
1134
+ });
1135
+ }
1136
+
1137
+ async pmTradesByMarket(params = {}) {
1138
+ const { marketId, pagination } = params;
1139
+ if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1140
+ return this.request('/api/v1/prediction-market/trades-by-market', {
1141
+ market_id: marketId,
1142
+ pagination
1143
+ });
1144
+ }
1145
+
1146
+ async pmTradesByAddress(params = {}) {
1147
+ const { address, pagination } = params;
1148
+ // Polymarket runs exclusively on Polygon
1149
+ const validation = validateAddress(address, 'polygon');
1150
+ if (!validation.valid) throw new NansenError(validation.error, validation.code);
1151
+ return this.request('/api/v1/prediction-market/trades-by-address', {
1152
+ address,
1153
+ pagination
1154
+ });
1155
+ }
1156
+
1157
+ async pmMarketScreener(params = {}) {
1158
+ const { sortBy = 'volume_24hr', query = '', status = '', pagination } = params;
1159
+ return this.request('/api/v1/prediction-market/market-screener', {
1160
+ sort_by: sortBy,
1161
+ query,
1162
+ status,
1163
+ pagination
1164
+ });
1165
+ }
1166
+
1167
+ async pmEventScreener(params = {}) {
1168
+ const { sortBy = 'volume_24hr', query = '', status = '', pagination } = params;
1169
+ return this.request('/api/v1/prediction-market/event-screener', {
1170
+ sort_by: sortBy,
1171
+ query,
1172
+ status,
1173
+ pagination
1174
+ });
1175
+ }
1176
+
1177
+ async pmPnlByMarket(params = {}) {
1178
+ const { marketId, pagination } = params;
1179
+ if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1180
+ return this.request('/api/v1/prediction-market/pnl-by-market', {
1181
+ market_id: marketId,
1182
+ pagination
1183
+ });
1184
+ }
1185
+
1186
+ async pmPnlByAddress(params = {}) {
1187
+ const { address, pagination } = params;
1188
+ // Polymarket runs exclusively on Polygon
1189
+ const validation = validateAddress(address, 'polygon');
1190
+ if (!validation.valid) throw new NansenError(validation.error, validation.code);
1191
+ return this.request('/api/v1/prediction-market/pnl-by-address', {
1192
+ address,
1193
+ pagination
1194
+ });
1195
+ }
1196
+
1197
+ async pmPositionDetail(params = {}) {
1198
+ const { marketId, pagination } = params;
1199
+ if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
1200
+ return this.request('/api/v1/prediction-market/position-detail', {
1201
+ market_id: marketId,
1202
+ pagination
1203
+ });
1204
+ }
1205
+
1206
+ async pmCategories(params = {}) {
1207
+ const { pagination } = params;
1208
+ return this.request('/api/v1/prediction-market/categories', {
1209
+ pagination
1210
+ });
1211
+ }
1212
+
1104
1213
  // ============= Points Endpoints =============
1105
1214
 
1106
1215
  async pointsLeaderboard(params = {}) {
package/src/cli.js CHANGED
@@ -55,15 +55,26 @@ export function filterFields(data, fields) {
55
55
  const filtered = {};
56
56
  for (const key of Object.keys(obj)) {
57
57
  if (fieldSet.has(key)) {
58
+ // Explicitly requested — include as-is
58
59
  filtered[key] = obj[key];
59
60
  } else if (typeof obj[key] === 'object' && obj[key] !== null) {
60
- // Recurse into nested objects/arrays
61
- const nested = filterObject(obj[key]);
62
- // Only include if it has content
63
- if (nested !== null && nested !== undefined) {
64
- if (Array.isArray(nested) && nested.length > 0) {
65
- filtered[key] = nested;
66
- } else if (!Array.isArray(nested) && Object.keys(nested).length > 0) {
61
+ if (Array.isArray(obj[key])) {
62
+ // Only recurse into arrays whose elements are plain objects.
63
+ // Primitive arrays (e.g. tags: ["a","b"]) are dropped unless the
64
+ // key was explicitly requested (handled above).
65
+ const hasObjectElements = obj[key].length > 0 &&
66
+ typeof obj[key][0] === 'object' && obj[key][0] !== null;
67
+ if (hasObjectElements) {
68
+ const nested = obj[key].map(item => filterObject(item))
69
+ .filter(item => Object.keys(item).length > 0);
70
+ if (nested.length > 0) {
71
+ filtered[key] = nested;
72
+ }
73
+ }
74
+ } else {
75
+ // Plain object — always recurse in case it wraps requested fields
76
+ const nested = filterObject(obj[key]);
77
+ if (nested !== null && nested !== undefined && Object.keys(nested).length > 0) {
67
78
  filtered[key] = nested;
68
79
  }
69
80
  }
@@ -439,6 +450,37 @@ async function enrichTransfers(result, apiInstance, chain) {
439
450
  return result;
440
451
  }
441
452
 
453
+ // ============= Address Parsing =============
454
+
455
+ /**
456
+ * Parse an --addresses option that may arrive as:
457
+ * - a pre-parsed array (arg parser split it)
458
+ * - a JSON array string: '["0x…","0x…"]'
459
+ * - a comma-separated string: "0x…,0x…"
460
+ * Non-array JSON values (objects, numbers, booleans) are rejected.
461
+ */
462
+ export function parseAddressList(raw) {
463
+ if (Array.isArray(raw)) {
464
+ return raw.map(a => String(a).trim()).filter(Boolean);
465
+ }
466
+ if (!raw) return [];
467
+
468
+ const s = String(raw);
469
+ try {
470
+ const parsed = JSON.parse(s);
471
+ if (Array.isArray(parsed)) {
472
+ return parsed.map(a => String(a).trim()).filter(Boolean);
473
+ }
474
+ throw new NansenError(
475
+ '--addresses must be a comma-separated list or JSON array, got: ' + typeof parsed,
476
+ ErrorCode.INVALID_PARAMS
477
+ );
478
+ } catch (e) {
479
+ if (e instanceof NansenError) throw e;
480
+ return s.split(',').map(a => a.trim()).filter(Boolean);
481
+ }
482
+ }
483
+
442
484
  // ============= Composite Functions =============
443
485
 
444
486
  export async function batchProfile(api, params = {}) {
@@ -644,7 +686,7 @@ EXAMPLES:
644
686
  nansen research profiler balance --address 0x... --chain ethereum
645
687
  nansen trade quote --chain base --from ETH --to USDC --amount 1
646
688
 
647
- Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
689
+ Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, zksync, mantle, ronin, sei, plasma, sonic, unichain, monad, hyperevm, iotaevm
648
690
  Trade chains: solana, base
649
691
  Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
650
692
 
@@ -780,6 +822,10 @@ export function buildCommands(deps = {}) {
780
822
  },
781
823
 
782
824
  'changelog': async (_args, _apiInstance, _flags, _options) => {
825
+ if (_flags.help || _flags.h) {
826
+ log('changelog — Show release history\n\nUsage:\n nansen changelog [--since <version>]\n\nOptions:\n --since <version> Show only entries for versions >= this version\n\nExamples:\n nansen changelog\n nansen changelog --since 1.10.0');
827
+ return;
828
+ }
783
829
  const changelogPath = new URL('../CHANGELOG.md', import.meta.url).pathname;
784
830
  let content;
785
831
  try {
@@ -795,10 +841,10 @@ export function buildCommands(deps = {}) {
795
841
  const filtered = [];
796
842
  let include = false;
797
843
  for (const line of lines) {
798
- // Match ## [x.y.z] headers
799
- const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
844
+ // Match ## [x.y.z] (Keep a Changelog format) or ## x.y.z (changeset format)
845
+ const match = line.match(/^## \[(\d+\.\d+\.\d+)\]|^## (\d+\.\d+\.\d+)\b/);
800
846
  if (match) {
801
- const ver = match[1];
847
+ const ver = match[1] || match[2];
802
848
  // Compare: include versions >= since, stop at versions < since
803
849
  if (compareSemver(ver, since) >= 0) {
804
850
  include = true;
@@ -945,7 +991,7 @@ export function buildCommands(deps = {}) {
945
991
  'batch': () => {
946
992
  let addresses = [];
947
993
  if (options.addresses) {
948
- addresses = options.addresses.split(',').map(a => a.trim()).filter(Boolean);
994
+ addresses = parseAddressList(options.addresses);
949
995
  } else if (options.file) {
950
996
  const content = fs.readFileSync(options.file, 'utf8');
951
997
  try {
@@ -976,13 +1022,13 @@ export function buildCommands(deps = {}) {
976
1022
  return traceCounterparties(apiInstance, { address, chain, depth, width, days, delayMs });
977
1023
  },
978
1024
  'compare': () => {
979
- const addrs = (options.addresses || '').split(',').map(a => a.trim()).filter(Boolean);
1025
+ const addrs = parseAddressList(options.addresses);
980
1026
  return compareWallets(apiInstance, { addresses: addrs, chain, days });
981
1027
  },
982
1028
  'help': () => ({
983
1029
  commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades', 'batch', 'trace', 'compare'],
984
1030
  description: 'Wallet profiling endpoints',
985
- example: 'nansen profiler balance --address 0x123... --chain ethereum'
1031
+ example: 'nansen research profiler compare --addresses "0xABC...,0xDEF..." --chain ethereum'
986
1032
  })
987
1033
  };
988
1034
 
@@ -1182,12 +1228,48 @@ export function buildCommands(deps = {}) {
1182
1228
  return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
1183
1229
  }
1184
1230
 
1231
+ return handlers[subcommand]();
1232
+ },
1233
+
1234
+ 'prediction-market': async (args, apiInstance, flags, options) => {
1235
+ const subcommand = args[0] || 'help';
1236
+ const marketId = options['market-id'];
1237
+ const address = options.address;
1238
+ const sortBy = options['sort-by'];
1239
+ const query = options.query;
1240
+ const status = options.status;
1241
+ const sort = parseSort(options.sort);
1242
+ const pagination = buildPagination(options);
1243
+
1244
+ const handlers = {
1245
+ 'ohlcv': () => apiInstance.pmOhlcv({ marketId, sort, pagination }),
1246
+ 'orderbook': () => apiInstance.pmOrderbook({ marketId, pagination }),
1247
+ 'top-holders': () => apiInstance.pmTopHolders({ marketId, sort, pagination }),
1248
+ 'trades-by-market': () => apiInstance.pmTradesByMarket({ marketId, pagination }),
1249
+ 'trades-by-address': () => apiInstance.pmTradesByAddress({ address, pagination }),
1250
+ 'market-screener': () => apiInstance.pmMarketScreener({ sortBy, query, status, pagination }),
1251
+ 'event-screener': () => apiInstance.pmEventScreener({ sortBy, query, status, pagination }),
1252
+ 'pnl-by-market': () => apiInstance.pmPnlByMarket({ marketId, pagination }),
1253
+ 'pnl-by-address': () => apiInstance.pmPnlByAddress({ address, pagination }),
1254
+ 'position-detail': () => apiInstance.pmPositionDetail({ marketId, pagination }),
1255
+ 'categories': () => apiInstance.pmCategories({ pagination }),
1256
+ 'help': () => ({
1257
+ commands: ['ohlcv', 'orderbook', 'top-holders', 'trades-by-market', 'trades-by-address', 'market-screener', 'event-screener', 'pnl-by-market', 'pnl-by-address', 'position-detail', 'categories'],
1258
+ description: 'Polymarket prediction market analytics',
1259
+ example: 'nansen research pm market-screener --sort-by volume_24hr --limit 20'
1260
+ })
1261
+ };
1262
+
1263
+ if (!handlers[subcommand]) {
1264
+ throw new NansenError(`Unknown subcommand: ${subcommand}. Available: ${Object.keys(handlers).filter(k => k !== 'help').join(', ')}`, ErrorCode.UNKNOWN);
1265
+ }
1266
+
1185
1267
  return handlers[subcommand]();
1186
1268
  }
1187
1269
  };
1188
1270
 
1189
1271
  // 'research' delegates to the category handlers defined above
1190
- const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points']);
1272
+ const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points', 'prediction-market']);
1191
1273
 
1192
1274
  cmds['research'] = async (args, apiInstance, flags, options) => {
1193
1275
  const rawCategory = args[0];
@@ -1263,11 +1345,12 @@ export const RESEARCH_CATEGORY_ALIASES = {
1263
1345
  'tgm': 'token',
1264
1346
  'sm': 'smart-money',
1265
1347
  'prof': 'profiler',
1266
- 'port': 'portfolio'
1348
+ 'port': 'portfolio',
1349
+ 'pm': 'prediction-market'
1267
1350
  };
1268
1351
 
1269
1352
  // Generate help text for a specific subcommand using SCHEMA
1270
- export function generateSubcommandHelp(command, subcommand) {
1353
+ export function generateSubcommandHelp(command, subcommand, prefix = null) {
1271
1354
  const cmdSchema = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
1272
1355
  if (!cmdSchema) return null;
1273
1356
 
@@ -1294,7 +1377,8 @@ export function generateSubcommandHelp(command, subcommand) {
1294
1377
 
1295
1378
  const exampleValues = { address: '0x...', token: '0x...', query: '"term"', symbol: 'BTC', date: '2024-01-01' };
1296
1379
  const chain = subSchema.options?.chain?.default || 'solana';
1297
- let example = `nansen ${command} ${subcommand}`;
1380
+ const cmdPrefix = prefix || (DEPRECATED_TO_RESEARCH.has(command) ? `research ${command}` : command);
1381
+ let example = `nansen ${cmdPrefix} ${subcommand}`;
1298
1382
  if (subSchema.options) {
1299
1383
  for (const [name, opt] of Object.entries(subSchema.options)) {
1300
1384
  if (opt.required) example += ` --${name} ${exampleValues[name] || '<val>'}`;
@@ -1362,7 +1446,7 @@ export async function runCLI(rawArgs, deps = {}) {
1362
1446
  const category = RESEARCH_CATEGORY_ALIASES[subcommand] || subcommand;
1363
1447
  const deepSub = subArgs[1];
1364
1448
  if (deepSub) {
1365
- const subHelp = generateSubcommandHelp(category, deepSub);
1449
+ const subHelp = generateSubcommandHelp(category, deepSub, `research ${subcommand}`);
1366
1450
  if (subHelp) {
1367
1451
  output(subHelp);
1368
1452
  notify();
@@ -1408,6 +1492,20 @@ export async function runCLI(rawArgs, deps = {}) {
1408
1492
  return { type: 'command-help', command };
1409
1493
  }
1410
1494
  }
1495
+ // Simple commands (logout, schema, cache) — show help instead of executing
1496
+ // Prevents destructive commands like logout from running when user just wants help
1497
+ if (commands[command]) {
1498
+ const simpleHelp = {
1499
+ 'logout': 'nansen logout — Remove saved API key from ~/.nansen/config.json',
1500
+ 'schema': 'nansen schema [command] [--pretty] — Show JSON schema for all commands (or a specific command)',
1501
+ 'cache': 'nansen cache clear — Clear the API response cache',
1502
+ };
1503
+ if (simpleHelp[command]) {
1504
+ output(simpleHelp[command]);
1505
+ notify();
1506
+ return { type: 'command-help', command };
1507
+ }
1508
+ }
1411
1509
  // Commands with handlers (e.g. quote, execute) show their own usage
1412
1510
  if (command === 'help' || !commands[command]) {
1413
1511
  output(BANNER + HELP);