nansen-cli 1.5.1 → 1.6.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/README.md CHANGED
@@ -123,6 +123,21 @@ Deep analytics for any token.
123
123
  |------------|-------------|
124
124
  | `defi` | DeFi holdings across protocols |
125
125
 
126
+ ### `search` - Search
127
+
128
+ Search for tokens and entities across Nansen.
129
+
130
+ ```bash
131
+ nansen search "uniswap" --pretty
132
+ nansen search "uniswap" --type token --chain ethereum
133
+ ```
134
+
135
+ | Option | Description |
136
+ |--------|-------------|
137
+ | `--type` | Filter by result type: `token`, `entity`, or `any` (default) |
138
+ | `--chain` | Filter by chain |
139
+ | `--limit` | Max results, 1-50 (default: 25) |
140
+
126
141
  ### `schema` - Schema Discovery
127
142
 
128
143
  Output JSON schema for agent introspection. No API key required.
@@ -258,7 +273,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines.
258
273
  | Profiler | 11 | 100% |
259
274
  | Token God Mode | 12 | 100% |
260
275
  | Portfolio | 1 | 100% |
261
- | **Total** | **30** | **100%** |
276
+ | Search | 1 | 100% |
277
+ | **Total** | **31** | **100%** |
262
278
 
263
279
  ## License
264
280
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.5.1",
3
+ "version": "1.6.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
@@ -9,6 +9,10 @@ import { fileURLToPath } from 'url';
9
9
 
10
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
 
12
+ const { version: packageVersion } = JSON.parse(
13
+ fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
14
+ );
15
+
12
16
  // ============= Error Codes =============
13
17
 
14
18
  /**
@@ -296,36 +300,35 @@ export function validateTokenAddress(tokenAddress, chain = 'solana') {
296
300
  }
297
301
 
298
302
  function loadConfig() {
299
- // Priority: 1. Environment variables, 2. ~/.nansen/config.json, 3. Local config.json
300
-
301
- // Check environment variables first (highest priority)
302
- if (process.env.NANSEN_API_KEY) {
303
- return {
304
- apiKey: process.env.NANSEN_API_KEY,
305
- baseUrl: process.env.NANSEN_BASE_URL || 'https://api.nansen.ai'
306
- };
307
- }
308
-
309
- // Check ~/.nansen/config.json (from `nansen login`)
303
+ // Base config from files, then env vars override individual fields
304
+ let config = null;
305
+
306
+ // ~/.nansen/config.json (from `nansen login`)
310
307
  if (fs.existsSync(CONFIG_FILE)) {
311
- try {
312
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
313
- } catch (e) {
314
- // Ignore parse errors, continue to next option
308
+ try { config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch (e) {}
309
+ }
310
+
311
+ // Local config.json (for development)
312
+ if (!config) {
313
+ const localConfig = path.join(__dirname, '..', 'config.json');
314
+ if (fs.existsSync(localConfig)) {
315
+ config = JSON.parse(fs.readFileSync(localConfig, 'utf8'));
315
316
  }
316
317
  }
317
-
318
- // Check local config.json (for development)
319
- const localConfig = path.join(__dirname, '..', 'config.json');
320
- if (fs.existsSync(localConfig)) {
321
- return JSON.parse(fs.readFileSync(localConfig, 'utf8'));
318
+
319
+ if (!config) {
320
+ config = { apiKey: null, baseUrl: 'https://api.nansen.ai' };
322
321
  }
323
-
324
- // No config found
325
- return {
326
- apiKey: null,
327
- baseUrl: 'https://api.nansen.ai'
328
- };
322
+
323
+ // Env vars override individual fields
324
+ if (process.env.NANSEN_API_KEY) {
325
+ config.apiKey = process.env.NANSEN_API_KEY;
326
+ }
327
+ if (process.env.NANSEN_BASE_URL) {
328
+ config.baseUrl = process.env.NANSEN_BASE_URL;
329
+ }
330
+
331
+ return config;
329
332
  }
330
333
 
331
334
  const config = loadConfig();
@@ -388,10 +391,11 @@ export class NansenAPI {
388
391
  this.apiKey = apiKey || null;
389
392
  this.baseUrl = baseUrl;
390
393
  this.retryOptions = { ...DEFAULT_RETRY_OPTIONS, ...options.retry };
391
- this.cacheOptions = {
394
+ this.cacheOptions = {
392
395
  enabled: options.cache?.enabled ?? false,
393
396
  ttl: options.cache?.ttl ?? DEFAULT_CACHE_TTL
394
397
  };
398
+ this.defaultHeaders = options.defaultHeaders || {};
395
399
  }
396
400
 
397
401
  static cleanBody(body) {
@@ -428,7 +432,10 @@ export class NansenAPI {
428
432
  method: 'POST',
429
433
  headers: {
430
434
  'Content-Type': 'application/json',
435
+ 'X-Client-Type': 'nansen-cli',
436
+ 'X-Client-Version': packageVersion,
431
437
  ...(this.apiKey ? { 'apikey': this.apiKey } : {}),
438
+ ...this.defaultHeaders,
432
439
  ...options.headers
433
440
  },
434
441
  body: JSON.stringify(NansenAPI.cleanBody(body))
@@ -482,7 +489,7 @@ export class NansenAPI {
482
489
  } else if (code === ErrorCode.CREDITS_EXHAUSTED) {
483
490
  message = message.replace(/\.+$/, '') + '. No retry will help. Check your Nansen dashboard for credit balance.';
484
491
  } else if (code === ErrorCode.PAYMENT_REQUIRED) {
485
- message = 'Payment required (x402). This endpoint requires on-chain payment.';
492
+ message = 'Payment required (x402). Sign the paymentRequirements below per https://docs.x402.org and pass the result with --x402-payment-signature <value>.';
486
493
  const paymentHeader = response.headers.get('payment-required');
487
494
  if (paymentHeader) {
488
495
  try {
@@ -668,6 +675,20 @@ export class NansenAPI {
668
675
  });
669
676
  }
670
677
 
678
+ async generalSearch(params = {}) {
679
+ const { query, resultType = 'any', chain, limit = 25 } = params;
680
+ if (!query) {
681
+ throw new NansenError('Search query is required', ErrorCode.MISSING_PARAM);
682
+ }
683
+ const body = {
684
+ search_query: query,
685
+ result_type: resultType,
686
+ limit
687
+ };
688
+ if (chain) body.chain = chain;
689
+ return this.request('/api/v1/search/general', body);
690
+ }
691
+
671
692
  async addressHistoricalBalances(params = {}) {
672
693
  const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
673
694
  if (address) {
@@ -954,8 +975,20 @@ export class NansenAPI {
954
975
  });
955
976
  }
956
977
 
978
+ async tokenIndicators(params = {}) {
979
+ const { tokenAddress, chain = 'ethereum' } = params;
980
+ if (tokenAddress) {
981
+ const validation = validateTokenAddress(tokenAddress, chain);
982
+ if (!validation.valid) throw new NansenError(validation.error, validation.code);
983
+ }
984
+ return this.request('/api/v1/tgm/indicators', {
985
+ token_address: tokenAddress,
986
+ chain
987
+ });
988
+ }
989
+
957
990
  async tokenInformation(params = {}) {
958
- const { tokenAddress, chain = 'solana', timeframe = '24h' } = params;
991
+ const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
959
992
  if (tokenAddress) {
960
993
  const validation = validateTokenAddress(tokenAddress, chain);
961
994
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
package/src/cli.js CHANGED
@@ -165,12 +165,20 @@ export const SCHEMA = {
165
165
  'token': {
166
166
  description: 'Token God Mode - deep analytics for any token',
167
167
  subcommands: {
168
+ 'indicators': {
169
+ description: 'Risk and reward indicators for a token (Nansen Score)',
170
+ options: {
171
+ token: { type: 'string', required: true, description: 'Token address' },
172
+ chain: { type: 'string', default: 'ethereum' }
173
+ },
174
+ 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]']
175
+ },
168
176
  'info': {
169
177
  description: 'Get detailed information for a specific token',
170
178
  options: {
171
179
  token: { type: 'string', required: true, description: 'Token address' },
172
180
  chain: { type: 'string', default: 'solana' },
173
- timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] }
181
+ timeframe: { type: 'string', default: '1d', enum: ['5m', '1h', '6h', '12h', '1d', '7d'] }
174
182
  },
175
183
  returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd']
176
184
  },
@@ -279,6 +287,16 @@ export const SCHEMA = {
279
287
  }
280
288
  }
281
289
  },
290
+ 'search': {
291
+ description: 'Search for tokens and entities across Nansen',
292
+ options: {
293
+ query: { type: 'string', required: true, description: 'Search query (token name, symbol, address, or entity)' },
294
+ type: { type: 'string', default: 'any', enum: ['token', 'entity', 'any'], description: 'Result type filter' },
295
+ chain: { type: 'string', description: 'Filter by chain (e.g., ethereum, solana)' },
296
+ limit: { type: 'number', default: 25, description: 'Max results (1-50)' }
297
+ },
298
+ returns: ['tokens[name, symbol, chain, address, price, volume_24h, market_cap, rank]', 'entities[name, tags, rank]', 'total_results']
299
+ },
282
300
  'points': {
283
301
  description: 'Nansen Points analytics',
284
302
  subcommands: {
@@ -299,7 +317,8 @@ export const SCHEMA = {
299
317
  fields: { type: 'string', description: 'Comma-separated list of fields to include in output' },
300
318
  'no-retry': { type: 'boolean', description: 'Disable automatic retry on rate limits/errors' },
301
319
  retries: { type: 'number', default: 3, description: 'Max retry attempts' },
302
- format: { type: 'string', enum: ['json', 'csv'], description: 'Output format (default: json)' }
320
+ format: { type: 'string', enum: ['json', 'csv'], description: 'Output format (default: json)' },
321
+ 'x402-payment-signature': { type: 'string', description: 'Pre-signed x402 payment signature header' }
303
322
  },
304
323
  chains: ['ethereum', 'solana', 'base', 'bnb', 'arbitrum', 'polygon', 'optimism', 'avalanche', 'linea', 'scroll', 'mantle', 'ronin', 'sei', 'plasma', 'sonic', 'monad', 'hyperevm', 'iotaevm'],
305
324
  smartMoneyLabels: ['Fund', 'Smart Trader', '30D Smart Trader', '90D Smart Trader', '180D Smart Trader', 'Smart HL Perps Trader']
@@ -825,9 +844,9 @@ COMMANDS:
825
844
  profiler Wallet profiling (balance, labels, transactions, pnl, pnl-summary, search,
826
845
  historical-balances, related-wallets, counterparties, perp-positions, perp-trades,
827
846
  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)
847
+ token Token God Mode (info, indicators, screener, holders, flows, dex-trades, pnl,
848
+ who-bought-sold, flow-intelligence, transfers, jup-dca, perp-trades,
849
+ perp-positions, perp-pnl-leaderboard)
831
850
  portfolio Portfolio analytics (defi)
832
851
  perp Perpetual futures analytics (screener, leaderboard)
833
852
  points Nansen Points analytics (leaderboard)
@@ -847,6 +866,7 @@ GLOBAL OPTIONS:
847
866
  --symbol Token symbol (for perp endpoints)
848
867
  --no-retry Disable automatic retry on rate limits/errors
849
868
  --retries <n> Max retry attempts (default: 3)
869
+ --x402-payment-signature <sig> Pre-signed x402 payment signature header
850
870
  --cache Enable response caching (default: off)
851
871
  --no-cache Disable cache for this request
852
872
  --cache-ttl <s> Cache TTL in seconds (default: 300)
@@ -1179,7 +1199,8 @@ export function buildCommands(deps = {}) {
1179
1199
  }
1180
1200
 
1181
1201
  const handlers = {
1182
- 'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe }),
1202
+ 'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
1203
+ 'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
1183
1204
  'screener': async () => {
1184
1205
  const search = options.search;
1185
1206
  // When searching, fetch more results to filter from (API has no server-side search)
@@ -1292,6 +1313,15 @@ export function buildCommands(deps = {}) {
1292
1313
  return handlers[subcommand]();
1293
1314
  },
1294
1315
 
1316
+ 'search': async (args, apiInstance, flags, options) => {
1317
+ return apiInstance.generalSearch({
1318
+ query: args[0] || options.query,
1319
+ resultType: options.type,
1320
+ chain: options.chain,
1321
+ limit: options.limit
1322
+ });
1323
+ },
1324
+
1295
1325
  'points': async (args, apiInstance, flags, options) => {
1296
1326
  const subcommand = args[0] || 'help';
1297
1327
  const tier = options.tier;
@@ -1538,7 +1568,11 @@ export async function runCLI(rawArgs, deps = {}) {
1538
1568
  ttl: options['cache-ttl'] !== undefined ? options['cache-ttl'] : 300
1539
1569
  };
1540
1570
 
1541
- const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions });
1571
+ const defaultHeaders = {};
1572
+ if (options['x402-payment-signature']) {
1573
+ defaultHeaders['Payment-Signature'] = options['x402-payment-signature'];
1574
+ }
1575
+ const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions, defaultHeaders });
1542
1576
  let result = await commands[command](subArgs, api, flags, options);
1543
1577
 
1544
1578
  // Apply field filtering if --fields is specified
@@ -1566,7 +1600,7 @@ export async function runCLI(rawArgs, deps = {}) {
1566
1600
  } catch (error) {
1567
1601
  const errorData = formatError(error);
1568
1602
  const formatted = formatOutput(errorData, { pretty, table, csv });
1569
- errorOutput(formatted.text);
1603
+ output(formatted.text);
1570
1604
  notify();
1571
1605
  exit(1);
1572
1606
  return { type: 'error', data: errorData };