nansen-cli 1.3.2 → 1.4.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/CLAUDE.md CHANGED
@@ -178,14 +178,38 @@ Structured error codes for programmatic handling:
178
178
  - **Beta endpoints** (`/api/beta/...`) may have different pagination
179
179
  - **EVM vs Solana addresses** — validation auto-detects based on chain param
180
180
 
181
- ## Changesets
181
+ ## Publishing (npm)
182
182
 
183
- Every PR that changes user-facing behavior must include a changeset:
184
- ```bash
185
- npx changeset
183
+ **⚠️ DO NOT manually run `npm version` or `npm publish`. CI handles everything.**
184
+
185
+ ### How it works:
186
+
187
+ 1. **Add a changeset** for user-facing changes:
188
+ ```bash
189
+ npx changeset
190
+ # Or manually create .changeset/<name>.md
191
+ ```
192
+
193
+ 2. **Push to main** — CI runs tests
194
+
195
+ 3. **CI creates a "Version Packages" PR** — This bumps version + updates CHANGELOG
196
+
197
+ 4. **Merge the Version PR** — CI auto-publishes to npm
198
+
199
+ ### Changeset format:
200
+ ```markdown
201
+ ---
202
+ "nansen-cli": minor
203
+ ---
204
+
205
+ Description of changes (appears in CHANGELOG)
186
206
  ```
187
- Choose `patch` for bug fixes, `minor` for new features, `major` for breaking changes.
188
- CI will not publish without a changeset.
207
+
208
+ Choose: `patch` (bug fixes), `minor` (new features), `major` (breaking changes)
209
+
210
+ ### If you mess up:
211
+ - Accidentally bumped version manually? `git revert` and add a changeset instead
212
+ - CI publish failed? Check GitHub Actions logs, likely needs `NPM_TOKEN` secret refresh
189
213
 
190
214
  ## PR Checklist
191
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/api.js CHANGED
@@ -634,14 +634,23 @@ export class NansenAPI {
634
634
  }
635
635
 
636
636
  async addressPnl(params = {}) {
637
- const { address, chain = 'ethereum' } = params;
637
+ const { address, chain = 'ethereum', date, days = 30, pagination } = params;
638
638
  if (address) {
639
639
  const validation = validateAddress(address, chain);
640
640
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
641
641
  }
642
- return this.request('/api/v1/profiler/address/pnl-and-trade-performance', {
642
+ // Build date range
643
+ let dateRange = date;
644
+ if (!dateRange) {
645
+ const to = new Date().toISOString().split('T')[0];
646
+ const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
647
+ dateRange = { from, to };
648
+ }
649
+ return this.request('/api/v1/profiler/address/pnl', {
643
650
  address,
644
- chain
651
+ chain,
652
+ date: dateRange,
653
+ pagination
645
654
  });
646
655
  }
647
656
 
@@ -938,6 +947,55 @@ export class NansenAPI {
938
947
  });
939
948
  }
940
949
 
950
+ async tokenInformation(params = {}) {
951
+ const { tokenAddress, chain = 'solana', timeframe = '24h' } = params;
952
+ if (tokenAddress) {
953
+ const validation = validateTokenAddress(tokenAddress, chain);
954
+ if (!validation.valid) throw new NansenError(validation.error, validation.code);
955
+ }
956
+ return this.request('/api/v1/tgm/token-information', {
957
+ token_address: tokenAddress,
958
+ chain,
959
+ timeframe
960
+ });
961
+ }
962
+
963
+ // ============= Perp Endpoints =============
964
+
965
+ async perpScreener(params = {}) {
966
+ const { filters = {}, orderBy, pagination, days = 30 } = params;
967
+ const to = new Date().toISOString().split('T')[0];
968
+ const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
969
+ return this.request('/api/v1/perp-screener', {
970
+ date: { from, to },
971
+ filters,
972
+ order_by: orderBy,
973
+ pagination
974
+ });
975
+ }
976
+
977
+ async perpLeaderboard(params = {}) {
978
+ const { filters = {}, orderBy, pagination, days = 30 } = params;
979
+ const to = new Date().toISOString().split('T')[0];
980
+ const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
981
+ return this.request('/api/v1/perp-leaderboard', {
982
+ date: { from, to },
983
+ filters,
984
+ order_by: orderBy,
985
+ pagination
986
+ });
987
+ }
988
+
989
+ // ============= Points Endpoints =============
990
+
991
+ async pointsLeaderboard(params = {}) {
992
+ const { tier, pagination } = params;
993
+ return this.request('/api/v1/points/leaderboard', {
994
+ tier,
995
+ pagination
996
+ });
997
+ }
998
+
941
999
  // ============= Portfolio Endpoints =============
942
1000
 
943
1001
  async portfolioDefiHoldings(params = {}) {
package/src/cli.js CHANGED
@@ -90,7 +90,7 @@ export const SCHEMA = {
90
90
  },
91
91
  'pnl': {
92
92
  description: 'PnL and trade performance',
93
- options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' } },
93
+ options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, date: { type: 'string', description: 'Date or date range (YYYY-MM-DD or {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"})' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
94
94
  returns: ['token_address', 'token_symbol', 'realized_pnl_usd', 'unrealized_pnl_usd', 'total_pnl_usd']
95
95
  },
96
96
  'search': {
@@ -165,6 +165,15 @@ export const SCHEMA = {
165
165
  'token': {
166
166
  description: 'Token God Mode - deep analytics for any token',
167
167
  subcommands: {
168
+ 'info': {
169
+ description: 'Get detailed information for a specific token',
170
+ options: {
171
+ token: { type: 'string', required: true, description: 'Token address' },
172
+ chain: { type: 'string', default: 'solana' },
173
+ timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] }
174
+ },
175
+ returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd']
176
+ },
168
177
  'screener': {
169
178
  description: 'Discover and filter tokens',
170
179
  options: {
@@ -172,6 +181,7 @@ export const SCHEMA = {
172
181
  chains: { type: 'array' },
173
182
  timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] },
174
183
  'smart-money': { type: 'boolean', description: 'Filter for Smart Money only' },
184
+ search: { type: 'string', description: 'Filter results by token symbol or name (client-side)' },
175
185
  limit: { type: 'number' },
176
186
  sort: { type: 'string' }
177
187
  },
@@ -243,6 +253,44 @@ export const SCHEMA = {
243
253
  returns: ['protocol', 'chain', 'position_type', 'token_symbol', 'balance', 'balance_usd']
244
254
  }
245
255
  }
256
+ },
257
+ 'perp': {
258
+ description: 'Perpetual futures analytics',
259
+ subcommands: {
260
+ 'screener': {
261
+ description: 'Screen perpetual futures contracts',
262
+ options: {
263
+ days: { type: 'number', default: 30 },
264
+ limit: { type: 'number' },
265
+ sort: { type: 'string' },
266
+ filters: { type: 'object' }
267
+ },
268
+ returns: ['token_symbol', 'volume_usd', 'open_interest', 'funding_rate', 'price_change_24h']
269
+ },
270
+ 'leaderboard': {
271
+ description: 'Perpetual futures PnL leaderboard',
272
+ options: {
273
+ days: { type: 'number', default: 30 },
274
+ limit: { type: 'number' },
275
+ sort: { type: 'string' },
276
+ filters: { type: 'object' }
277
+ },
278
+ returns: ['address', 'address_label', 'realized_pnl', 'unrealized_pnl', 'total_pnl', 'trade_count', 'win_rate']
279
+ }
280
+ }
281
+ },
282
+ 'points': {
283
+ description: 'Nansen Points analytics',
284
+ subcommands: {
285
+ 'leaderboard': {
286
+ description: 'Points leaderboard',
287
+ options: {
288
+ tier: { type: 'string', description: 'Filter by tier' },
289
+ limit: { type: 'number' }
290
+ },
291
+ returns: ['rank', 'address', 'address_label', 'points', 'tier']
292
+ }
293
+ }
246
294
  }
247
295
  },
248
296
  globalOptions: {
@@ -773,10 +821,16 @@ COMMANDS:
773
821
  logout Remove saved API key
774
822
  schema Output JSON schema for all commands (for agent introspection)
775
823
  cache Cache management (clear)
776
- smart-money Smart Money analytics (netflow, dex-trades, holdings, dcas, historical-holdings)
777
- profiler Wallet profiling (balance, labels, pnl, batch, trace, compare, counterparties)
778
- token Token God Mode (screener, holders, flows, trades, pnl, perp-trades, perp-positions)
779
- portfolio Portfolio analytics (defi-holdings)
824
+ smart-money Smart Money analytics (netflow, dex-trades, perp-trades, holdings, dcas, historical-holdings)
825
+ profiler Wallet profiling (balance, labels, transactions, pnl, pnl-summary, search,
826
+ historical-balances, related-wallets, counterparties, perp-positions, perp-trades,
827
+ batch, trace, compare)
828
+ token Token God Mode (info, screener, holders, flows, dex-trades, pnl, who-bought-sold,
829
+ flow-intelligence, transfers, jup-dca, perp-trades, perp-positions,
830
+ perp-pnl-leaderboard)
831
+ portfolio Portfolio analytics (defi)
832
+ perp Perpetual futures analytics (screener, leaderboard)
833
+ points Nansen Points analytics (leaderboard)
780
834
  help Show this help message
781
835
 
782
836
  GLOBAL OPTIONS:
@@ -1043,7 +1097,10 @@ export function buildCommands(deps = {}) {
1043
1097
  const date = parseDateOption(options.date, days);
1044
1098
  return apiInstance.addressTransactions({ address, chain, filters, orderBy, pagination, days, date });
1045
1099
  },
1046
- 'pnl': () => apiInstance.addressPnl({ address, chain }),
1100
+ 'pnl': () => {
1101
+ const date = parseDateOption(options.date, days);
1102
+ return apiInstance.addressPnl({ address, chain, date, days, pagination });
1103
+ },
1047
1104
  'search': () => apiInstance.entitySearch({ query: options.query }),
1048
1105
  'historical-balances': () => apiInstance.addressHistoricalBalances({ address, chain, filters, orderBy, pagination, days }),
1049
1106
  'related-wallets': () => apiInstance.addressRelatedWallets({ address, chain, orderBy, pagination }),
@@ -1122,7 +1179,21 @@ export function buildCommands(deps = {}) {
1122
1179
  }
1123
1180
 
1124
1181
  const handlers = {
1125
- 'screener': () => apiInstance.tokenScreener({ chains, timeframe, filters, orderBy, pagination }),
1182
+ 'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe }),
1183
+ 'screener': async () => {
1184
+ const result = await apiInstance.tokenScreener({ chains, timeframe, filters, orderBy, pagination });
1185
+ // Client-side search filter (API doesn't support server-side search)
1186
+ const search = options.search;
1187
+ if (search && result?.data) {
1188
+ const q = search.toLowerCase();
1189
+ const filtered = result.data.filter(t =>
1190
+ (t.token_symbol && t.token_symbol.toLowerCase().includes(q)) ||
1191
+ (t.token_name && t.token_name.toLowerCase().includes(q))
1192
+ );
1193
+ return { ...result, data: filtered };
1194
+ }
1195
+ return result;
1196
+ },
1126
1197
  'holders': () => apiInstance.tokenHolders({ tokenAddress, chain, labelType: onlySmartMoney ? 'smart_money' : 'all_holders', filters, orderBy, pagination }),
1127
1198
  'flows': () => {
1128
1199
  const date = parseDateOption(options.date, days);
@@ -1146,7 +1217,7 @@ export function buildCommands(deps = {}) {
1146
1217
  'perp-positions': () => apiInstance.tokenPerpPositions({ tokenSymbol, filters, orderBy, pagination }),
1147
1218
  'perp-pnl-leaderboard': () => apiInstance.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days }),
1148
1219
  'help': () => ({
1149
- commands: ['screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
1220
+ commands: ['info', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
1150
1221
  description: 'Token God Mode endpoints',
1151
1222
  example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
1152
1223
  })
@@ -1184,6 +1255,51 @@ export function buildCommands(deps = {}) {
1184
1255
  return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
1185
1256
  }
1186
1257
 
1258
+ return handlers[subcommand]();
1259
+ },
1260
+
1261
+ 'perp': async (args, apiInstance, flags, options) => {
1262
+ const subcommand = args[0] || 'help';
1263
+ const filters = options.filters || {};
1264
+ const orderBy = parseSort(options.sort, options['order-by']);
1265
+ const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
1266
+ const days = options.days ? parseInt(options.days) : 30;
1267
+
1268
+ const handlers = {
1269
+ 'screener': () => apiInstance.perpScreener({ filters, orderBy, pagination, days }),
1270
+ 'leaderboard': () => apiInstance.perpLeaderboard({ filters, orderBy, pagination, days }),
1271
+ 'help': () => ({
1272
+ commands: ['screener', 'leaderboard'],
1273
+ description: 'Perpetual futures analytics endpoints',
1274
+ example: 'nansen perp screener --days 7 --limit 20'
1275
+ })
1276
+ };
1277
+
1278
+ if (!handlers[subcommand]) {
1279
+ return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
1280
+ }
1281
+
1282
+ return handlers[subcommand]();
1283
+ },
1284
+
1285
+ 'points': async (args, apiInstance, flags, options) => {
1286
+ const subcommand = args[0] || 'help';
1287
+ const tier = options.tier;
1288
+ const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
1289
+
1290
+ const handlers = {
1291
+ 'leaderboard': () => apiInstance.pointsLeaderboard({ tier, pagination }),
1292
+ 'help': () => ({
1293
+ commands: ['leaderboard'],
1294
+ description: 'Nansen Points analytics endpoints',
1295
+ example: 'nansen points leaderboard --limit 100'
1296
+ })
1297
+ };
1298
+
1299
+ if (!handlers[subcommand]) {
1300
+ return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
1301
+ }
1302
+
1187
1303
  return handlers[subcommand]();
1188
1304
  }
1189
1305
  };
@@ -1192,6 +1308,120 @@ export function buildCommands(deps = {}) {
1192
1308
  // Commands that don't require API authentication
1193
1309
  export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache'];
1194
1310
 
1311
+ // Command aliases for convenience
1312
+ export const COMMAND_ALIASES = {
1313
+ 'tgm': 'token', // Token God Mode
1314
+ 'sm': 'smart-money', // Smart Money
1315
+ 'prof': 'profiler', // Profiler
1316
+ 'port': 'portfolio' // Portfolio
1317
+ };
1318
+
1319
+ // Generate help text for a specific subcommand using SCHEMA
1320
+ export function generateSubcommandHelp(command, subcommand) {
1321
+ const cmdSchema = SCHEMA.commands[command];
1322
+ if (!cmdSchema) return null;
1323
+
1324
+ const subSchema = cmdSchema.subcommands?.[subcommand];
1325
+ if (!subSchema) return null;
1326
+
1327
+ const lines = [];
1328
+ lines.push(`\n${command} ${subcommand} - ${subSchema.description || 'No description'}\n`);
1329
+
1330
+ // Usage
1331
+ const requiredOpts = [];
1332
+ const optionalOpts = [];
1333
+
1334
+ if (subSchema.options) {
1335
+ for (const [name, opt] of Object.entries(subSchema.options)) {
1336
+ if (opt.required) {
1337
+ requiredOpts.push(name);
1338
+ } else {
1339
+ optionalOpts.push(name);
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ let usage = `USAGE:\n nansen ${command} ${subcommand}`;
1345
+ if (requiredOpts.length) {
1346
+ usage += ' ' + requiredOpts.map(o => `--${o} <value>`).join(' ');
1347
+ }
1348
+ if (optionalOpts.length) {
1349
+ usage += ' [options]';
1350
+ }
1351
+ lines.push(usage);
1352
+
1353
+ // Required options
1354
+ if (requiredOpts.length) {
1355
+ lines.push('\nREQUIRED:');
1356
+ for (const name of requiredOpts) {
1357
+ const opt = subSchema.options[name];
1358
+ const desc = opt.description || `${opt.type}`;
1359
+ lines.push(` --${name.padEnd(16)} ${desc}`);
1360
+ }
1361
+ }
1362
+
1363
+ // Optional options
1364
+ if (optionalOpts.length) {
1365
+ lines.push('\nOPTIONS:');
1366
+ for (const name of optionalOpts) {
1367
+ const opt = subSchema.options[name];
1368
+ const defaultStr = opt.default !== undefined ? ` (default: ${opt.default})` : '';
1369
+ const desc = (opt.description || opt.type) + defaultStr;
1370
+ lines.push(` --${name.padEnd(16)} ${desc}`);
1371
+ }
1372
+ }
1373
+
1374
+ // Return fields
1375
+ if (subSchema.returns && subSchema.returns.length) {
1376
+ lines.push('\nRETURNS:');
1377
+ lines.push(` ${subSchema.returns.join(', ')}`);
1378
+ }
1379
+
1380
+ // Examples
1381
+ lines.push('\nEXAMPLES:');
1382
+ const chain = subSchema.options?.chain?.default || 'solana';
1383
+
1384
+ // Example values for common required options
1385
+ const exampleValues = {
1386
+ address: '0x123...',
1387
+ token: '0x123...',
1388
+ query: '"search term"',
1389
+ symbol: 'BTC',
1390
+ date: '2024-01-01'
1391
+ };
1392
+
1393
+ // Build example based on required options
1394
+ let example = ` nansen ${command} ${subcommand}`;
1395
+ for (const name of requiredOpts) {
1396
+ const value = exampleValues[name] || '<value>';
1397
+ example += ` --${name} ${value}`;
1398
+ }
1399
+ if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
1400
+ example += ` --chain ${chain}`;
1401
+ }
1402
+ example += ' --pretty';
1403
+ lines.push(example);
1404
+
1405
+ // Add a filtered example if filters are supported
1406
+ if (subSchema.options?.filters || subSchema.options?.labels) {
1407
+ let filterExample = ` nansen ${command} ${subcommand}`;
1408
+ for (const name of requiredOpts) {
1409
+ const value = exampleValues[name] || '<value>';
1410
+ filterExample += ` --${name} ${value}`;
1411
+ }
1412
+ if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
1413
+ filterExample += ` --chain ${chain}`;
1414
+ }
1415
+ if (subSchema.options?.labels) {
1416
+ filterExample += ' --labels "Smart Trader"';
1417
+ }
1418
+ filterExample += ' --limit 10 --table';
1419
+ lines.push(filterExample);
1420
+ }
1421
+
1422
+ return lines.join('\n');
1423
+ }
1424
+
1195
1425
  // Run CLI with given args (returns result, allows custom output/exit handlers)
1196
1426
  export async function runCLI(rawArgs, deps = {}) {
1197
1427
  const {
@@ -1204,8 +1434,11 @@ export async function runCLI(rawArgs, deps = {}) {
1204
1434
 
1205
1435
  const { _: positional, flags, options } = parseArgs(rawArgs);
1206
1436
 
1207
- const command = positional[0] || 'help';
1437
+ // Resolve command aliases
1438
+ const rawCommand = positional[0] || 'help';
1439
+ const command = COMMAND_ALIASES[rawCommand] || rawCommand;
1208
1440
  const subArgs = positional.slice(1);
1441
+ const subcommand = subArgs[0];
1209
1442
  const pretty = flags.pretty || flags.p;
1210
1443
  const table = flags.table || flags.t;
1211
1444
  const stream = flags.stream || flags.s;
@@ -1224,6 +1457,32 @@ export async function runCLI(rawArgs, deps = {}) {
1224
1457
  }
1225
1458
 
1226
1459
  if (command === 'help' || flags.help || flags.h) {
1460
+ // Check for subcommand-specific help: nansen <command> <subcommand> --help
1461
+ if (flags.help || flags.h) {
1462
+ // First try subcommand help
1463
+ if (command && subcommand) {
1464
+ const subHelp = generateSubcommandHelp(command, subcommand);
1465
+ if (subHelp) {
1466
+ output(subHelp);
1467
+ notify();
1468
+ return { type: 'subcommand-help', command, subcommand };
1469
+ }
1470
+ }
1471
+ // Then try command-level help (list subcommands)
1472
+ if (command && SCHEMA.commands[command]) {
1473
+ const cmdSchema = SCHEMA.commands[command];
1474
+ const lines = [`\n${command} - ${cmdSchema.description}\n`];
1475
+ lines.push('SUBCOMMANDS:');
1476
+ for (const [sub, subSchema] of Object.entries(cmdSchema.subcommands || {})) {
1477
+ lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
1478
+ }
1479
+ lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
1480
+ output(lines.join('\n'));
1481
+ notify();
1482
+ return { type: 'command-help', command };
1483
+ }
1484
+ }
1485
+ // Fallback to main help
1227
1486
  output(BANNER + HELP);
1228
1487
  notify();
1229
1488
  return { type: 'help' };