nansen-cli 1.4.0 → 1.5.1

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.
Files changed (3) hide show
  1. package/package.json +4 -4
  2. package/src/api.js +15 -8
  3. package/src/cli.js +17 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -34,12 +34,12 @@
34
34
  "license": "MIT",
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "git+https://github.com/askeluv/nansen-cli.git"
37
+ "url": "git+https://github.com/nansen-ai/nansen-cli.git"
38
38
  },
39
39
  "bugs": {
40
- "url": "https://github.com/askeluv/nansen-cli/issues"
40
+ "url": "https://github.com/nansen-ai/nansen-cli/issues"
41
41
  },
42
- "homepage": "https://github.com/askeluv/nansen-cli#readme",
42
+ "homepage": "https://github.com/nansen-ai/nansen-cli#readme",
43
43
  "engines": {
44
44
  "node": ">=18.0.0"
45
45
  },
package/src/api.js CHANGED
@@ -19,6 +19,7 @@ export const ErrorCode = {
19
19
  UNAUTHORIZED: 'UNAUTHORIZED', // 401 - Invalid or missing API key
20
20
  FORBIDDEN: 'FORBIDDEN', // 403 - Valid key but insufficient permissions
21
21
  CREDITS_EXHAUSTED: 'CREDITS_EXHAUSTED', // 403 - Insufficient API credits
22
+ PAYMENT_REQUIRED: 'PAYMENT_REQUIRED', // 402 - x402 payment required
22
23
 
23
24
  // Rate Limiting
24
25
  RATE_LIMITED: 'RATE_LIMITED', // 429 - Too many requests
@@ -87,6 +88,8 @@ function statusToErrorCode(status, data = {}) {
87
88
  return ErrorCode.INVALID_PARAMS;
88
89
  case 401:
89
90
  return ErrorCode.UNAUTHORIZED;
91
+ case 402:
92
+ return ErrorCode.PAYMENT_REQUIRED;
90
93
  case 403:
91
94
  if (messageLower.includes('credit') || messageLower.includes('insufficient')) return ErrorCode.CREDITS_EXHAUSTED;
92
95
  return ErrorCode.FORBIDDEN;
@@ -382,13 +385,7 @@ function parseRetryAfter(headerValue) {
382
385
 
383
386
  export class NansenAPI {
384
387
  constructor(apiKey = config.apiKey, baseUrl = config.baseUrl, options = {}) {
385
- if (!apiKey) {
386
- throw new NansenError(
387
- 'API key required. Run `nansen login` or set NANSEN_API_KEY environment variable.',
388
- ErrorCode.UNAUTHORIZED
389
- );
390
- }
391
- this.apiKey = apiKey;
388
+ this.apiKey = apiKey || null;
392
389
  this.baseUrl = baseUrl;
393
390
  this.retryOptions = { ...DEFAULT_RETRY_OPTIONS, ...options.retry };
394
391
  this.cacheOptions = {
@@ -431,7 +428,7 @@ export class NansenAPI {
431
428
  method: 'POST',
432
429
  headers: {
433
430
  'Content-Type': 'application/json',
434
- 'apikey': this.apiKey,
431
+ ...(this.apiKey ? { 'apikey': this.apiKey } : {}),
435
432
  ...options.headers
436
433
  },
437
434
  body: JSON.stringify(NansenAPI.cleanBody(body))
@@ -484,6 +481,16 @@ export class NansenAPI {
484
481
  message = message.replace(/\.+$/, '') + '. This filter is not supported for this token/chain combination. Do not retry.';
485
482
  } else if (code === ErrorCode.CREDITS_EXHAUSTED) {
486
483
  message = message.replace(/\.+$/, '') + '. No retry will help. Check your Nansen dashboard for credit balance.';
484
+ } else if (code === ErrorCode.PAYMENT_REQUIRED) {
485
+ message = 'Payment required (x402). This endpoint requires on-chain payment.';
486
+ const paymentHeader = response.headers.get('payment-required');
487
+ if (paymentHeader) {
488
+ try {
489
+ data.paymentRequirements = JSON.parse(atob(paymentHeader));
490
+ } catch {
491
+ data.paymentRequiredRaw = paymentHeader;
492
+ }
493
+ }
487
494
  }
488
495
 
489
496
  lastError = new NansenError(message, code, response.status, {
package/src/cli.js CHANGED
@@ -1181,16 +1181,26 @@ export function buildCommands(deps = {}) {
1181
1181
  const handlers = {
1182
1182
  'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe }),
1183
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
1184
  const search = options.search;
1187
- if (search && result?.data) {
1185
+ // When searching, fetch more results to filter from (API has no server-side search)
1186
+ const searchPagination = search
1187
+ ? { page: 1, per_page: Math.max(500, pagination?.per_page || 0) }
1188
+ : pagination;
1189
+ const result = await apiInstance.tokenScreener({ chains, timeframe, filters, orderBy, pagination: searchPagination });
1190
+ if (search) {
1188
1191
  const q = search.toLowerCase();
1189
- const filtered = result.data.filter(t =>
1192
+ const requestedLimit = pagination?.per_page || 100;
1193
+ const filterArr = (arr) => arr.filter(t =>
1190
1194
  (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 };
1195
+ (t.token_name && t.token_name.toLowerCase().includes(q)) ||
1196
+ (t.token_address && t.token_address.toLowerCase() === q)
1197
+ ).slice(0, requestedLimit);
1198
+ // Handle nested response shapes: {data: [...]} or {data: {data: [...]}}
1199
+ if (Array.isArray(result?.data)) {
1200
+ return { ...result, data: filterArr(result.data) };
1201
+ } else if (result?.data?.data && Array.isArray(result.data.data)) {
1202
+ return { ...result, data: { ...result.data, data: filterArr(result.data.data) } };
1203
+ }
1194
1204
  }
1195
1205
  return result;
1196
1206
  },