@bankofai/x402-cli 1.0.1-beta.4 → 1.0.1-beta.5

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
@@ -13,6 +13,15 @@ from the payment token, so the payer does not need TRX.
13
13
 
14
14
  ## Install
15
15
 
16
+ Install the CLI package:
17
+
18
+ ```bash
19
+ npm install -g @bankofai/x402-cli
20
+ x402-cli --version
21
+ ```
22
+
23
+ For repository development:
24
+
16
25
  ```bash
17
26
  npm install
18
27
  npm run build
@@ -61,8 +70,8 @@ The server exposes:
61
70
 
62
71
  - `GET /health`
63
72
  - `GET /.well-known/x402`
64
- - `GET /pay` returns `402 Payment Required`
65
- - `POST /pay` verifies and settles with the facilitator
73
+ - `/pay` returns `402 Payment Required` without a payment signature
74
+ - The signed retry uses the same HTTP method, then verifies and settles with the facilitator
66
75
 
67
76
  ### Pay
68
77
 
@@ -75,6 +84,9 @@ node dist/cli.js pay http://127.0.0.1:4020/pay \
75
84
  --token USDT
76
85
  ```
77
86
 
87
+ For automated or unfamiliar endpoints, set `--max-amount` or
88
+ `--max-raw-amount` before allowing the CLI to sign a payment.
89
+
78
90
  Pay a TRON GasFree endpoint (the CLI normally selects this automatically from
79
91
  the server challenge):
80
92
 
@@ -90,6 +102,12 @@ Use `--gasfree-api-url <url>` or `X402_GASFREE_API_URL` to override the SDK's
90
102
  default relayer endpoint.
91
103
 
92
104
  For EVM networks use `EVM_PRIVATE_KEY` or `PRIVATE_KEY`.
105
+ Prefer environment variables over `--private-key` in shared environments,
106
+ because command-line arguments may be visible to other local processes.
107
+
108
+ If the gateway settles a payment but the upstream request fails, JSON error
109
+ output includes `error.details.paymentResponse` for reconciliation. Do not retry
110
+ such a request blindly; inspect the transaction and provider behavior first.
93
111
 
94
112
  ### Roundtrip
95
113
 
package/dist/cli.js CHANGED
@@ -18,11 +18,13 @@ class CliError extends Error {
18
18
  code;
19
19
  hint;
20
20
  exitCode;
21
- constructor(code, message, hint, exitCode = 1) {
21
+ details;
22
+ constructor(code, message, hint, exitCode = 1, details) {
22
23
  super(message);
23
24
  this.code = code;
24
25
  this.hint = hint;
25
26
  this.exitCode = exitCode;
27
+ this.details = details;
26
28
  }
27
29
  }
28
30
  function parseArgs(argv) {
@@ -144,6 +146,8 @@ function emit(args) {
144
146
  process.stderr.write(` ${args.error.message}\n`);
145
147
  if (args.error.hint)
146
148
  process.stderr.write(` hint: ${args.error.hint}\n`);
149
+ if (args.error.details !== undefined)
150
+ process.stderr.write(` details: ${JSON.stringify(args.error.details)}\n`);
147
151
  return;
148
152
  }
149
153
  const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
@@ -171,6 +175,7 @@ function classify(error) {
171
175
  code: error.code,
172
176
  message,
173
177
  hint: error.hint,
178
+ details: error.details,
174
179
  };
175
180
  }
176
181
  const lower = message.toLowerCase();
@@ -737,7 +742,7 @@ async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS,
737
742
  return await fetch(input, { ...init, signal: controller.signal });
738
743
  }
739
744
  catch (error) {
740
- if (error?.name === "AbortError")
745
+ if (error instanceof Error && error.name === "AbortError")
741
746
  throw new Error(`${label} timed out after ${timeout}ms`);
742
747
  throw error;
743
748
  }
@@ -1182,7 +1187,13 @@ function selectRequirement(accepts, options) {
1182
1187
  if (scheme && scheme !== req.scheme)
1183
1188
  return false;
1184
1189
  if (token) {
1185
- const tokenInfo = getToken(req.network, token);
1190
+ let tokenInfo;
1191
+ try {
1192
+ tokenInfo = getToken(req.network, token);
1193
+ }
1194
+ catch {
1195
+ return false;
1196
+ }
1186
1197
  if (tokenInfo.address.toLowerCase() !== req.asset.toLowerCase())
1187
1198
  return false;
1188
1199
  }
@@ -1266,7 +1277,9 @@ async function pay(url, options) {
1266
1277
  };
1267
1278
  if (!paid.ok) {
1268
1279
  const retryAfter = paid.headers.get("retry-after");
1269
- throw new Error(`HTTP ${paid.status} from ${url}${retryAfter ? ` (retry after ${retryAfter}s)` : ""}: ${typeof body === "string" ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`);
1280
+ throw new CliError(paid.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR", `HTTP ${paid.status} from ${url}${retryAfter ? ` (retry after ${retryAfter}s)` : ""}: ${typeof body === "string" ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`, paymentResponse
1281
+ ? "The gateway reports that payment was settled; retain paymentResponse for support or reconciliation."
1282
+ : "Inspect the HTTP response and retry only when it is safe to do so.", 1, result);
1270
1283
  }
1271
1284
  emit({
1272
1285
  command: "client",
@@ -1278,8 +1291,7 @@ async function pay(url, options) {
1278
1291
  }
1279
1292
  async function roundtrip(options) {
1280
1293
  const port = Number(opt(options, "port", "4020"));
1281
- serve(options);
1282
- await delay(500);
1294
+ await serve(options);
1283
1295
  await pay(`http://127.0.0.1:${port}/pay`, options);
1284
1296
  process.exit(0);
1285
1297
  }
@@ -104,15 +104,21 @@ export function loadProvider(file) {
104
104
  validateProvider(config, file);
105
105
  config.operator.network = normalizeNetwork(config.operator.network);
106
106
  normalizePaymentProtocol(config, file);
107
+ const facilitatorUrl = config.operator.facilitator_url ||
108
+ process.env.X402_FACILITATOR_URL ||
109
+ process.env.FACILITATOR_URL ||
110
+ "https://facilitator.bankofai.io";
111
+ assertHttpUrl(facilitatorUrl, `${file}: facilitator URL`);
112
+ const configuredApiKeyEnv = config.operator.facilitator_api_key_env;
113
+ if (configuredApiKeyEnv && !process.env[configuredApiKeyEnv]) {
114
+ throw new Error(`${file}: environment variable ${configuredApiKeyEnv} is not set`);
115
+ }
107
116
  return {
108
117
  config,
109
- facilitatorUrl: config.operator.facilitator_url ||
110
- process.env.X402_FACILITATOR_URL ||
111
- process.env.FACILITATOR_URL ||
112
- "https://facilitator.bankofai.io",
118
+ facilitatorUrl,
113
119
  facilitatorApiKey: config.operator.facilitator_api_key ||
114
- (config.operator.facilitator_api_key_env
115
- ? process.env[config.operator.facilitator_api_key_env]
120
+ (configuredApiKeyEnv
121
+ ? process.env[configuredApiKeyEnv]
116
122
  : undefined) ||
117
123
  process.env.X402_FACILITATOR_API_KEY ||
118
124
  process.env.FACILITATOR_API_KEY,
@@ -18,9 +18,20 @@ class RequestTooLargeError extends HttpError {
18
18
  super(413, "request body too large");
19
19
  }
20
20
  }
21
- const MAX_BODY_BYTES = Number(process.env.X402_GATEWAY_MAX_BODY_BYTES ?? 1_000_000);
22
- const FACILITATOR_TIMEOUT_MS = Number(process.env.X402_GATEWAY_FACILITATOR_TIMEOUT_MS ?? 10_000);
23
- const UPSTREAM_TIMEOUT_MS = Number(process.env.X402_GATEWAY_UPSTREAM_TIMEOUT_MS ?? 30_000);
21
+ function positiveIntegerEnv(name, fallback) {
22
+ const raw = process.env[name];
23
+ if (raw === undefined || raw === "")
24
+ return fallback;
25
+ if (!/^\d+$/.test(raw))
26
+ throw new Error(`${name} must be a positive integer`);
27
+ const value = Number(raw);
28
+ if (!Number.isSafeInteger(value) || value <= 0)
29
+ throw new Error(`${name} must be a positive integer`);
30
+ return value;
31
+ }
32
+ const MAX_BODY_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_BODY_BYTES", 1_000_000);
33
+ const FACILITATOR_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_FACILITATOR_TIMEOUT_MS", 10_000);
34
+ const UPSTREAM_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_UPSTREAM_TIMEOUT_MS", 30_000);
24
35
  const STRIP_REQUEST_HEADERS = new Set([
25
36
  "host",
26
37
  "connection",
@@ -48,14 +59,22 @@ const STRIP_RESPONSE_HEADERS = new Set([
48
59
  "authorization",
49
60
  "proxy-authorization",
50
61
  "set-cookie",
62
+ "payment-required",
63
+ "x-payment-required",
64
+ "payment-signature",
65
+ "x-payment",
66
+ "payment-response",
67
+ "x-payment-response",
51
68
  ]);
52
- const metrics = {
53
- requests: 0,
54
- paidRequests: 0,
55
- verifyFailures: 0,
56
- settleFailures: 0,
57
- upstreamFailures: 0,
58
- };
69
+ function createMetrics() {
70
+ return {
71
+ requests: 0,
72
+ paidRequests: 0,
73
+ verifyFailures: 0,
74
+ settleFailures: 0,
75
+ upstreamFailures: 0,
76
+ };
77
+ }
59
78
  async function readBody(request) {
60
79
  const chunks = [];
61
80
  let total = 0;
@@ -79,7 +98,7 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
79
98
  return await fetch(url, { ...init, signal: controller.signal });
80
99
  }
81
100
  catch (error) {
82
- if (error?.name === "AbortError")
101
+ if (error instanceof Error && error.name === "AbortError")
83
102
  throw new HttpError(504, `${label} request timed out`);
84
103
  throw error;
85
104
  }
@@ -112,12 +131,25 @@ async function facilitatorPost(entry, path, body) {
112
131
  }
113
132
  return data;
114
133
  }
115
- function resourceUrl(url) {
134
+ function configuredPublicBaseUrl() {
135
+ const value = process.env.X402_GATEWAY_PUBLIC_BASE_URL?.trim();
136
+ if (!value)
137
+ return undefined;
138
+ try {
139
+ const url = new URL(value);
140
+ if (!["http:", "https:"].includes(url.protocol))
141
+ throw new Error("unsupported protocol");
142
+ return `${value.replace(/\/+$/, "")}/`;
143
+ }
144
+ catch {
145
+ throw new Error("X402_GATEWAY_PUBLIC_BASE_URL must be a valid http(s) URL");
146
+ }
147
+ }
148
+ function resourceUrl(url, publicBaseUrl) {
116
149
  const path = `${url.pathname}${url.search}`;
117
- const publicBaseUrl = process.env.X402_GATEWAY_PUBLIC_BASE_URL?.trim();
118
150
  if (!publicBaseUrl)
119
151
  return path;
120
- return new URL(path, `${publicBaseUrl.replace(/\/+$/, "")}/`).toString();
152
+ return new URL(path, publicBaseUrl).toString();
121
153
  }
122
154
  function logFacilitatorFailure(entry, path, response, body, data) {
123
155
  const requirement = body?.paymentRequirements;
@@ -205,7 +237,7 @@ function upstreamUrl(entry, request, routePath) {
205
237
  }
206
238
  return upstream;
207
239
  }
208
- async function forward(entry, request, response, routePath, body, paymentResponse) {
240
+ async function forward(metrics, entry, request, response, routePath, body, paymentResponse) {
209
241
  const upstream = upstreamUrl(entry, request, routePath);
210
242
  let upstreamResponse;
211
243
  try {
@@ -229,10 +261,13 @@ async function forward(entry, request, response, routePath, body, paymentRespons
229
261
  });
230
262
  if (paymentResponse)
231
263
  responseHeaders[headers.response] = encodeResponse(paymentResponse);
264
+ const responseBody = Buffer.from(await upstreamResponse.arrayBuffer());
232
265
  response.writeHead(upstreamResponse.status, responseHeaders);
233
- response.end(Buffer.from(await upstreamResponse.arrayBuffer()));
266
+ response.end(responseBody);
234
267
  }
235
268
  export function createGatewayServer(providers) {
269
+ const publicBaseUrl = configuredPublicBaseUrl();
270
+ const metrics = createMetrics();
236
271
  return http.createServer(async (request, response) => {
237
272
  try {
238
273
  metrics.requests += 1;
@@ -302,7 +337,7 @@ export function createGatewayServer(providers) {
302
337
  const body = await readBody(request);
303
338
  const requirements = paymentRequirements(entry.config, priceUsd(endpoint, requestParams(url, body, request)));
304
339
  if (!requirements.length) {
305
- await forward(entry, request, response, routePath, body);
340
+ await forward(metrics, entry, request, response, routePath, body);
306
341
  return;
307
342
  }
308
343
  const paymentHeader = request.headers[headers.signature.toLowerCase()];
@@ -311,7 +346,7 @@ export function createGatewayServer(providers) {
311
346
  const challenge = {
312
347
  x402Version: 2,
313
348
  error: "Payment required",
314
- resource: { url: resourceUrl(url) },
349
+ resource: { url: resourceUrl(url, publicBaseUrl) },
315
350
  accepts,
316
351
  };
317
352
  json(response, 402, challenge, { [headers.required]: encodeRequired(challenge) });
@@ -356,7 +391,20 @@ export function createGatewayServer(providers) {
356
391
  return;
357
392
  }
358
393
  metrics.paidRequests += 1;
359
- await forward(entry, request, response, routePath, body, settle);
394
+ try {
395
+ await forward(metrics, entry, request, response, routePath, body, settle);
396
+ }
397
+ catch (error) {
398
+ const status = error instanceof HttpError ? error.status : 502;
399
+ const extraHeaders = error instanceof HttpError ? error.responseHeaders : {};
400
+ json(response, status, {
401
+ error: "upstream failed after payment settlement",
402
+ settled: true,
403
+ }, {
404
+ ...extraHeaders,
405
+ [headers.response]: encodeResponse(settle),
406
+ });
407
+ }
360
408
  }
361
409
  catch (error) {
362
410
  if (error instanceof HttpError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bankofai/x402-cli",
3
- "version": "1.0.1-beta.4",
3
+ "version": "1.0.1-beta.5",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@bankofai/x402-core": "1.0.1-beta.4",
24
24
  "@bankofai/x402-evm": "1.0.1-beta.4",
25
- "@bankofai/x402-gateway": "1.0.1-beta.4",
25
+ "@bankofai/x402-gateway": "1.0.1-beta.5",
26
26
  "@bankofai/x402-tron": "1.0.1-beta.4",
27
27
  "tronweb": "6.4.0",
28
28
  "viem": "^2.55.0",
@@ -32,5 +32,8 @@
32
32
  "@types/node": "^24.10.1",
33
33
  "tsx": "^4.20.6",
34
34
  "typescript": "^5.9.3"
35
+ },
36
+ "overrides": {
37
+ "ws": "8.21.0"
35
38
  }
36
39
  }