@bankofai/x402-cli 1.0.1-beta.7 → 1.0.1-beta.9

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/dist/cli.js CHANGED
@@ -1,979 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import http from "node:http";
3
- import { setTimeout as delay } from "node:timers/promises";
4
- import fs from "node:fs";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { spawn } from "node:child_process";
8
- import { createRequire } from "node:module";
9
3
  import { fileURLToPath } from "node:url";
10
- import YAML from "yaml";
11
4
  import { createPaymentPayload, decodeRequired, decodeResponse, decodeSignature, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
12
5
  import { assertRawAmount, findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
13
- const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "raw", "version"]);
14
- const require = createRequire(import.meta.url);
15
- const DEFAULT_TIMEOUT_MS = 30_000;
16
- const CATALOG_UPDATE_RETRIES = 3;
17
- class CliError extends Error {
18
- code;
19
- hint;
20
- exitCode;
21
- details;
22
- constructor(code, message, hint, exitCode = 1, details) {
23
- super(message);
24
- this.code = code;
25
- this.hint = hint;
26
- this.exitCode = exitCode;
27
- this.details = details;
28
- }
29
- }
30
- function parseArgs(argv) {
31
- const [command = "help", ...rest] = argv;
32
- const positional = [];
33
- const options = {};
34
- for (let i = 0; i < rest.length; i += 1) {
35
- const item = rest[i];
36
- if (item === "-h") {
37
- options.help = true;
38
- continue;
39
- }
40
- if (item === "-V") {
41
- options.version = true;
42
- continue;
43
- }
44
- if (item === "-d") {
45
- options.daemon = true;
46
- continue;
47
- }
48
- if (item === "-n") {
49
- const next = rest[i + 1];
50
- if (!next || next.startsWith("-")) {
51
- options.limit = true;
52
- }
53
- else {
54
- options.limit = next;
55
- i += 1;
56
- }
57
- continue;
58
- }
59
- if (!item.startsWith("--")) {
60
- positional.push(item);
61
- continue;
62
- }
63
- const eq = item.indexOf("=");
64
- const key = eq > 2 ? item.slice(2, eq) : item.slice(2);
65
- const inline = eq > 2 ? item.slice(eq + 1) : undefined;
66
- const next = rest[i + 1];
67
- if (inline !== undefined) {
68
- options[key] = inline;
69
- }
70
- else if (BOOLEAN_FLAGS.has(key)) {
71
- options[key] = true;
72
- }
73
- else if (!next || next.startsWith("--")) {
74
- options[key] = true;
75
- }
76
- else {
77
- if (key === "header") {
78
- const current = options[key];
79
- options[key] = Array.isArray(current) ? [...current, next] : current ? [String(current), next] : [next];
80
- }
81
- else {
82
- options[key] = next;
83
- }
84
- i += 1;
85
- }
86
- }
87
- return { command, positional, options };
88
- }
89
- function getVersion() {
90
- try {
91
- const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
92
- return String(pkg.version ?? "0.0.0");
93
- }
94
- catch {
95
- return "0.0.0";
96
- }
97
- }
98
- function opt(options, key, fallback) {
99
- const value = options[key];
100
- return typeof value === "string" ? value : fallback;
101
- }
102
- function hasFlag(options, key) {
103
- return options[key] === true;
104
- }
105
- function outputMode(options) {
106
- if (hasFlag(options, "json") && hasFlag(options, "human")) {
107
- throw new CliError("INVALID_ARGUMENT", "--json and --human are mutually exclusive", "Pass either --json or --human, not both.", 2);
108
- }
109
- return hasFlag(options, "json") ? "json" : "human";
110
- }
111
- function requireArgument(value, name, usage) {
112
- if (value === undefined || value === "") {
113
- throw new CliError("MISSING_ARGUMENT", `${name} is required`, `Usage: ${usage}`, 2);
114
- }
115
- return value;
116
- }
117
- function optAll(options, key) {
118
- const value = options[key];
119
- if (Array.isArray(value))
120
- return value;
121
- return typeof value === "string" ? [value] : [];
122
- }
123
- function printJson(value) {
124
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
125
- }
126
- function emit(args) {
127
- const mode = args.mode ?? "human";
128
- if (mode === "json") {
129
- const envelope = {
130
- ok: !args.error,
131
- command: args.command,
132
- };
133
- if (args.network)
134
- envelope.network = args.network;
135
- if (args.scheme)
136
- envelope.scheme = args.scheme;
137
- if (args.error)
138
- envelope.error = args.error;
139
- else
140
- envelope.result = args.result ?? null;
141
- printJson(envelope);
142
- return;
143
- }
144
- if (args.error) {
145
- process.stderr.write(`ERROR ${args.command}: ${args.error.code}\n`);
146
- process.stderr.write(` ${args.error.message}\n`);
147
- if (args.error.hint)
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`);
151
- return;
152
- }
153
- const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
154
- process.stdout.write(`OK ${args.command}${suffix ? ` (${suffix})` : ""}\n`);
155
- if (args.result && typeof args.result === "object" && !Array.isArray(args.result)) {
156
- for (const [key, value] of Object.entries(args.result)) {
157
- if (value === undefined)
158
- continue;
159
- if (value && typeof value === "object") {
160
- process.stdout.write(` ${key}: ${JSON.stringify(value)}\n`);
161
- }
162
- else {
163
- process.stdout.write(` ${key}: ${value}\n`);
164
- }
165
- }
166
- }
167
- else if (args.result !== undefined) {
168
- process.stdout.write(` ${args.result}\n`);
169
- }
170
- }
171
- function classify(error) {
172
- const message = error instanceof Error ? error.message : String(error);
173
- if (error instanceof CliError) {
174
- return {
175
- code: error.code,
176
- message,
177
- hint: error.hint,
178
- details: error.details,
179
- };
180
- }
181
- const lower = message.toLowerCase();
182
- if (lower.includes("missing private key") || lower.includes("could not find a wallet")) {
183
- return {
184
- code: "WALLET_NOT_CONFIGURED",
185
- message,
186
- hint: "Set PRIVATE_KEY, TRON_PRIVATE_KEY, EVM_PRIVATE_KEY, or configure agent-wallet with a payer wallet.",
187
- };
188
- }
189
- if (lower.includes("wallets_config") || lower.includes("wallet config")) {
190
- return {
191
- code: "WALLET_CONFIG_CORRUPT",
192
- message,
193
- hint: "Check ~/.agent-wallet/wallets_config.json or recreate the local agent-wallet configuration.",
194
- };
195
- }
196
- if (lower.includes("does not exist") && lower.includes("account [t")) {
197
- return {
198
- code: "TRON_ACCOUNT_NOT_ACTIVATED",
199
- message,
200
- hint: "Activate the TRON address by sending it a small amount of TRX before signing contract calls.",
201
- };
202
- }
203
- if (lower.includes("permit2_insufficient_balance") || lower.includes("insufficient") && lower.includes("balance")) {
204
- return {
205
- code: "INSUFFICIENT_TOKEN_BALANCE",
206
- message,
207
- hint: "Fund the payer address with the exact token and network advertised by the provider, then retry.",
208
- };
209
- }
210
- if (lower.includes("transfer_from_failed") || lower.includes("transferfrom failed")) {
211
- return {
212
- code: "TOKEN_TRANSFER_FAILED",
213
- message,
214
- hint: "Check token balance, token contract, payer address, and that the selected x402 route matches the provider requirement.",
215
- };
216
- }
217
- if (lower.includes("insufficient funds for gas") || lower.includes("insufficient gas") || lower.includes("energy")) {
218
- return {
219
- code: "INSUFFICIENT_GAS",
220
- message,
221
- hint: "Fund the payer address with the native gas token for this network.",
222
- };
223
- }
224
- if (lower.includes("deadline") || lower.includes("expired")) {
225
- return {
226
- code: "DEADLINE_OR_CLOCK_SKEW",
227
- message,
228
- hint: "Check local clock sync and retry with a fresh payment requirement.",
229
- };
230
- }
231
- if (lower.includes("permittransferfrom") || lower.includes("invalid signature") || lower.includes("permit reverted")) {
232
- return {
233
- code: "PERMIT_REVERTED",
234
- message,
235
- hint: "The token or Permit2 contract rejected the signature; retry with a fresh requirement and verify token/network support.",
236
- };
237
- }
238
- if (lower.includes("tokenregistry") && lower.includes("import")) {
239
- return {
240
- code: "SDK_API_DRIFT",
241
- message,
242
- hint: "Installed x402 SDK packages do not match this CLI; reinstall @bankofai/x402-cli and SDK dependencies.",
243
- };
244
- }
245
- if (lower.includes("429") || lower.includes("too many requests") || lower.includes("rate limit")) {
246
- return {
247
- code: "RATE_LIMITED",
248
- message,
249
- hint: "Wait briefly and retry; the upstream service or RPC is rate limiting requests.",
250
- };
251
- }
252
- if (lower.includes("402 response missing")) {
253
- return {
254
- code: "INVALID_X402_RESPONSE",
255
- message,
256
- hint: "The endpoint returned HTTP 402 without a PAYMENT-REQUIRED header.",
257
- };
258
- }
259
- if (lower.includes("no matching payment requirement")) {
260
- return {
261
- code: "NO_MATCHING_PAYMENT_REQUIREMENT",
262
- message,
263
- hint: "Relax --network, --token, or --scheme, or use values offered by the provider.",
264
- };
265
- }
266
- if (lower.includes("exceeds --max")) {
267
- return {
268
- code: "PAYMENT_AMOUNT_TOO_HIGH",
269
- message,
270
- hint: "Increase the max amount flag only if this provider price is expected.",
271
- };
272
- }
273
- if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused") || lower.includes("timed out")) {
274
- return {
275
- code: "NETWORK_ERROR",
276
- message,
277
- hint: "Check the URL, local server, proxy, and network connectivity.",
278
- };
279
- }
280
- if (lower.includes(" is required") || lower.includes("must be") || lower.includes("invalid --") || lower.includes("mutually exclusive")) {
281
- return {
282
- code: lower.includes("required") ? "MISSING_ARGUMENT" : "INVALID_ARGUMENT",
283
- message,
284
- hint: "Run the command with --help to see valid usage and options.",
285
- };
286
- }
287
- return {
288
- code: "IO_ERROR",
289
- message,
290
- hint: "Run with --json for structured output, and check the provider/gateway logs for details.",
291
- };
292
- }
293
- function helpText(topic = "root") {
294
- const sections = {
295
- root: `x402-cli ${getVersion()}
296
-
297
- Usage:
298
- x402-cli <command> [options]
299
-
300
- Commands:
301
- pay <url> Pay an x402-protected URL
302
- serve Run a local x402 paywall endpoint
303
- roundtrip Start serve, pay it, then exit
304
- gateway <command> Manage local gateway provider files
305
- catalog <command> Search, cache, and export provider catalog assets
306
-
307
- Global options:
308
- -h, --help Show help
309
- -V, --version Show version
310
- --json Print machine-readable JSON envelope
311
- --human Print human-readable output (default)
312
- `,
313
- pay: `Usage:
314
- x402-cli pay <url> [options]
315
-
316
- Options:
317
- --method <method> HTTP method (default: GET)
318
- --header "Name: Value" Request header, repeatable
319
- --body <body> Request body for non-GET/HEAD methods
320
- --network <caip2> Require a specific network
321
- --token <symbol> Require a specific token
322
- --scheme <scheme> Require a specific x402 scheme
323
- --gasfree-api-url <url> Override the TRON GasFree relayer API URL
324
- --max-gasfree-fee <amt> Maximum GasFree relayer fee in token units
325
- --max-gasfree-fee-raw <n> Maximum GasFree relayer fee in smallest units
326
- --max-amount <amount> Maximum human-readable payment amount
327
- --max-raw-amount <amount> Maximum smallest-unit payment amount
328
- --dry-run Read requirements but do not sign or pay
329
- --private-key <hex> Explicit payer private key (or PRIVATE_KEY/TRON_PRIVATE_KEY/EVM_PRIVATE_KEY)
330
- --rpc-url <url> Explicit network RPC URL
331
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
332
- --json Print JSON envelope
333
-
334
- Examples:
335
- x402-cli pay https://api.example.com/paid --dry-run --json
336
- x402-cli pay https://api.example.com/paid --max-amount 0.01
337
- `,
338
- serve: `Usage:
339
- x402-cli serve --pay-to <address> [options]
340
-
341
- Options:
342
- --pay-to <address> Recipient wallet address
343
- --amount <amount> Human-readable token amount (default: 0.0001)
344
- --raw-amount <amount> Smallest-unit amount
345
- --network <caip2> Payment network (default: tron:0xcd8690dc)
346
- --scheme <scheme> Payment scheme: exact or exact_gasfree (default: exact)
347
- --token <symbol> Token symbol (default: USDT)
348
- --asset <address> Explicit token address
349
- --decimals <count> Token decimals for unregistered --asset
350
- --host <host> Bind host (default: 127.0.0.1)
351
- --port <port> Bind port (default: 4020)
352
- --resource-url <url> URL advertised in payment requirements
353
- --facilitator-url <url> Facilitator base URL
354
- --timeout-ms <ms> Facilitator timeout in milliseconds (default: 30000)
355
- --daemon Run in background and print the child pid
356
- --json Print JSON envelope
357
-
358
- Examples:
359
- x402-cli serve --pay-to T... --network tron:0xcd8690dc --token USDT
360
- x402-cli serve --pay-to 0x... --network eip155:97 --token USDT --amount 0.0001
361
- `,
362
- roundtrip: `Usage:
363
- x402-cli roundtrip --pay-to <address> [serve/pay options]
364
- `,
365
- gateway: `Usage:
366
- x402-cli gateway <search|start|check|scaffold|catalog> [options]
367
-
368
- Commands:
369
- search <query> Search a gateway/catalog artifact
370
- start Start a local x402 gateway process
371
- check <providers> Validate provider.yml files
372
- scaffold <name> Write a starter provider.yml
373
- catalog <command> Build/check/search gateway catalog assets
374
- `,
375
- "gateway-catalog": `Usage:
376
- x402-cli gateway catalog <build|check|pay-assets|search> [options]
377
-
378
- Commands:
379
- build <providers> Build a local catalog from provider.yml files
380
- check <providers> Validate local provider.yml files
381
- pay-assets <providers> List payable endpoint assets
382
- search <query> Search a catalog artifact
383
- `,
384
- catalog: `Usage:
385
- x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build> [options]
386
-
387
- Commands:
388
- update Cache hosted/local catalog assets under ~/.cache
389
- search <query> Search providers
390
- show <provider> Show provider detail JSON
391
- endpoints <provider> List provider endpoints
392
- pay-json <provider> Print provider pay JSON
393
- export-gateway <url> Export catalog.json and pay.md from a gateway
394
- build <providers> Build catalog from provider.yml files
395
-
396
- Options:
397
- --catalog <source> catalog.json path or URL
398
- --provider <fqn> Provider FQN for export-gateway
399
- --output-dir <dir> Output directory for generated files
400
- -n, --limit <count> Search result limit
401
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
402
- --include-blocked Include blocked providers in search
403
- --json Print JSON envelope
404
- `,
405
- "catalog-search": `Usage:
406
- x402-cli catalog search <query> [--catalog <source>] [options]
407
-
408
- Options:
409
- --catalog <source> catalog.json path or URL
410
- -n, --limit <count> Search result limit
411
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
412
- --include-blocked Include blocked providers in search
413
- --json Print JSON envelope
414
- `,
415
- "catalog-show": `Usage:
416
- x402-cli catalog show <provider> [--catalog <source>] [options]
417
-
418
- Options:
419
- --catalog <source> catalog.json path or URL
420
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
421
- --json Print JSON envelope
422
- `,
423
- "catalog-pay-json": `Usage:
424
- x402-cli catalog pay-json <provider> [--catalog <source>] [options]
425
-
426
- Options:
427
- --catalog <source> catalog.json path or URL
428
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
429
- --raw Print raw pay payload instead of JSON envelope
430
- --json Print JSON envelope
431
- `,
432
- "catalog-endpoints": `Usage:
433
- x402-cli catalog endpoints <provider> [--catalog <source>] [options]
434
-
435
- Options:
436
- --catalog <source> catalog.json path or URL
437
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
438
- --json Print JSON envelope
439
- `,
440
- "catalog-export-gateway": `Usage:
441
- x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]
442
-
443
- Options:
444
- --provider <fqn> Provider FQN to export
445
- --output-dir <dir> Output directory for generated files
446
- --force Overwrite existing files
447
- --json Print JSON envelope
448
- `,
449
- };
450
- return sections[topic] ?? sections.root;
451
- }
452
- function readYaml(file) {
453
- return YAML.parse(fs.readFileSync(file, "utf8"));
454
- }
455
- function expandEnv(value) {
456
- return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
457
- if (!(name in process.env))
458
- throw new Error(`environment variable \${${name}} is not set`);
459
- return process.env[name] ?? "";
460
- });
461
- }
462
- function expandDeep(value) {
463
- if (typeof value === "string")
464
- return expandEnv(value);
465
- if (Array.isArray(value))
466
- return value.map(expandDeep);
467
- if (value && typeof value === "object") {
468
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandDeep(item)]));
469
- }
470
- return value;
471
- }
472
- function providerFiles(root) {
473
- const stat = fs.statSync(root);
474
- if (stat.isFile())
475
- return [root];
476
- const out = [];
477
- for (const entry of fs.readdirSync(root, { recursive: true })) {
478
- const file = path.join(root, String(entry));
479
- if (file.endsWith("provider.yml") || file.endsWith("provider.yaml"))
480
- out.push(file);
481
- }
482
- return out.sort();
483
- }
484
- function loadProviderFile(file) {
485
- const provider = expandDeep(readYaml(file));
486
- validateProvider(provider, file);
487
- provider.operator.network = normalizeNetwork(provider.operator.network);
488
- provider.operator.scheme = provider.operator.scheme ?? "exact";
489
- return provider;
490
- }
491
- function validateProvider(provider, file = "provider.yml") {
492
- const required = [
493
- ["name", provider?.name],
494
- ["forward_url", provider?.forward_url],
495
- ["operator.network", provider?.operator?.network],
496
- ["operator.recipient", provider?.operator?.recipient],
497
- ];
498
- for (const [name, value] of required) {
499
- if (typeof value !== "string" || !value.trim())
500
- throw new Error(`${file}: ${name} is required`);
501
- }
502
- try {
503
- const url = new URL(provider.forward_url);
504
- if (!["http:", "https:"].includes(url.protocol))
505
- throw new Error("unsupported protocol");
506
- }
507
- catch {
508
- throw new Error(`${file}: forward_url must be a valid http(s) URL`);
509
- }
510
- if (!Array.isArray(provider.endpoints) || !provider.endpoints.length) {
511
- throw new Error(`${file}: endpoints must contain at least one endpoint`);
512
- }
513
- const seen = new Set();
514
- for (const endpoint of provider.endpoints) {
515
- if (typeof endpoint.method !== "string" || typeof endpoint.path !== "string") {
516
- throw new Error(`${file}: each endpoint needs method and path`);
517
- }
518
- if (!endpoint.method.trim() || !endpoint.path.trim() || !endpoint.path.startsWith("/")) {
519
- throw new Error(`${file}: endpoint method/path must be non-empty and path must start with /`);
520
- }
521
- const price = providerPrice(endpoint);
522
- if (!Number.isFinite(price) || price < 0)
523
- throw new Error(`${file}: endpoint price_usd must be a finite number >= 0`);
524
- const key = `${endpoint.method.toUpperCase()} ${endpoint.path}`;
525
- if (seen.has(key))
526
- throw new Error(`${file}: duplicate endpoint ${key}`);
527
- seen.add(key);
528
- }
529
- }
530
- function providerPrice(endpoint) {
531
- return endpoint?.metering?.dimensions?.[0]?.tiers?.[0]?.price_usd ?? 0;
532
- }
533
- function providerAssetTransferMethod(provider) {
534
- return provider.operator?.asset_transfer_method ?? provider.operator?.assetTransferMethod ?? "permit2";
535
- }
536
- function providerScheme(provider) {
537
- return provider.operator?.scheme ?? "exact";
538
- }
539
- function providerCatalog(provider) {
540
- return {
541
- name: provider.name,
542
- title: provider.title ?? provider.name,
543
- description: provider.description ?? "",
544
- category: provider.category ?? "other",
545
- service_url: provider.display?.service_url,
546
- tags: provider.display?.tags ?? [],
547
- network: normalizeNetwork(provider.operator.network),
548
- currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
549
- endpoints: (provider.endpoints ?? []).map((endpoint) => ({
550
- method: endpoint.method.toUpperCase(),
551
- path: `/providers/${provider.name}${endpoint.path}`,
552
- upstream_path: endpoint.path,
553
- description: endpoint.description ?? "",
554
- paid: providerPrice(endpoint) > 0 ? {
555
- scheme: providerScheme(provider),
556
- network: normalizeNetwork(provider.operator.network),
557
- currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
558
- price_usd: providerPrice(endpoint),
559
- } : null,
560
- x402_routes: providerPrice(endpoint) > 0 ? [{
561
- provider: provider.name,
562
- network: normalizeNetwork(provider.operator.network),
563
- scheme: providerScheme(provider),
564
- assetTransferMethod: providerAssetTransferMethod(provider),
565
- url: `/providers/${provider.name}${endpoint.path}`,
566
- }] : [],
567
- })),
568
- };
569
- }
570
- const FIELD_WEIGHTS = {
571
- fqn: 12,
572
- title: 10,
573
- tags: 8,
574
- chain_kinds: 8,
575
- chains: 8,
576
- category: 6,
577
- category_meta: 6,
578
- endpoints: 6,
579
- i18n: 5,
580
- description: 4,
581
- use_case: 4,
582
- service_url: 2,
583
- };
584
- function stringList(value) {
585
- return Array.isArray(value) ? value.filter(item => item != null).map(String) : [];
586
- }
587
- function dictValues(value) {
588
- if (!value || typeof value !== "object")
589
- return [];
590
- const out = [];
591
- for (const child of Object.values(value)) {
592
- if (child && typeof child === "object" && !Array.isArray(child))
593
- out.push(...dictValues(child));
594
- else if (Array.isArray(child))
595
- out.push(...child.filter(item => item != null).map(String));
596
- else if (child != null)
597
- out.push(String(child));
598
- }
599
- return out;
600
- }
601
- function chainMetaValues(chainsMeta) {
602
- return chainsMeta.flatMap(dictValues);
603
- }
604
- function endpointFields(endpoints) {
605
- const values = [];
606
- for (const endpoint of endpoints) {
607
- values.push(String(endpoint.method ?? ""), String(endpoint.path ?? ""), String(endpoint.probe_status ?? ""));
608
- const paid = endpoint.paid;
609
- if (paid && typeof paid === "object") {
610
- values.push(String(paid.network ?? ""), String(paid.currency ?? ""), String(paid.amount_raw ?? ""));
611
- }
612
- values.push(String(endpoint.title ?? ""), String(endpoint.description ?? ""), String(endpoint.use_case ?? endpoint.useCase ?? ""));
613
- }
614
- return values;
615
- }
616
- function scoreFields(terms, fields) {
617
- let score = 0;
618
- const matchedFields = [];
619
- for (const [field, values] of Object.entries(fields)) {
620
- const haystack = values.filter(Boolean).join(" ").toLowerCase();
621
- if (!haystack)
622
- continue;
623
- const count = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
624
- if (count) {
625
- score += (FIELD_WEIGHTS[field] ?? 1) * count;
626
- matchedFields.push(field);
627
- }
628
- }
629
- return { score, matchedFields };
630
- }
631
- async function readCatalog(source, options) {
632
- const text = await readText(source, options);
633
- const parsed = JSON.parse(text);
634
- if (Array.isArray(parsed))
635
- return parsed;
636
- if (Array.isArray(parsed.providers))
637
- return parsed.providers;
638
- if (Array.isArray(parsed.items))
639
- return parsed.items;
640
- return [];
641
- }
642
- async function readCatalogObject(source, options) {
643
- const parsed = JSON.parse(await readText(source, options));
644
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
645
- throw new Error(`expected JSON object from ${source}`);
646
- }
647
- return parsed;
648
- }
649
- async function readText(source, options) {
650
- if (!source.startsWith("http://") && !source.startsWith("https://")) {
651
- return fs.readFileSync(source, "utf8");
652
- }
653
- const response = await fetchWithTimeout(source, {}, timeoutMs(options), `fetch ${source}`);
654
- if (!response.ok)
655
- throw new Error(`failed to fetch ${source}: ${response.status}`);
656
- return response.text();
657
- }
658
- async function readJson(source, options) {
659
- return JSON.parse(await readText(source, options));
660
- }
661
- function writeJson(file, value) {
662
- fs.mkdirSync(path.dirname(file), { recursive: true });
663
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
664
- }
665
- async function responsePayload(response) {
666
- const text = await response.text();
667
- const contentType = response.headers.get("content-type") ?? "";
668
- if (contentType.toLowerCase().includes("json")) {
669
- try {
670
- return JSON.parse(text);
671
- }
672
- catch {
673
- return text;
674
- }
675
- }
676
- return text;
677
- }
678
- async function withSdkStdoutRedirect(enabled, fn) {
679
- if (!enabled)
680
- return fn();
681
- const originalLog = console.log;
682
- console.log = (...args) => {
683
- process.stderr.write(`${args.map(arg => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ")}\n`);
684
- };
685
- try {
686
- return await fn();
687
- }
688
- finally {
689
- console.log = originalLog;
690
- }
691
- }
692
- function cacheDir() {
693
- return path.join(os.homedir(), ".cache", "x402-cli", "catalog");
694
- }
695
- function cachedCatalogPath() {
696
- return path.join(cacheDir(), "catalog.json");
697
- }
698
- function providerFilename(fqn) {
699
- return `${fqn.replace(/\//g, "__")}.json`;
700
- }
701
- function sanitizeProviderName(name) {
702
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(name) || name.includes("..")) {
703
- throw new Error("provider name must be a safe FQN using letters, numbers, dots, underscores, dashes, or slashes");
704
- }
705
- return name;
706
- }
707
- function safeOutputPath(baseDir, ...parts) {
708
- const root = path.resolve(baseDir);
709
- const target = path.resolve(root, ...parts);
710
- if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
711
- throw new Error(`refusing to write outside output directory: ${target}`);
712
- }
713
- return target;
714
- }
715
- function ensureWritable(file, options) {
716
- if (fs.existsSync(file) && !hasFlag(options, "force")) {
717
- throw new Error(`${file} already exists; pass --force to overwrite`);
718
- }
719
- }
720
- function defaultCatalogSource() {
721
- const envSource = process.env.X402_CATALOG || process.env.X402_GATEWAY_CATALOG;
722
- if (envSource)
723
- return envSource;
724
- return fs.existsSync(cachedCatalogPath())
725
- ? cachedCatalogPath()
726
- : "https://x402-catalog.bankofai.io/api/catalog.json";
727
- }
728
- function positiveIntegerOption(options, key, fallback) {
729
- const value = opt(options, key, String(fallback));
730
- if (!/^\d+$/.test(value))
731
- throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
732
- const parsed = Number(value);
733
- if (!Number.isSafeInteger(parsed) || parsed <= 0)
734
- throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
735
- return parsed;
736
- }
737
- function timeoutMs(options) {
738
- return options ? positiveIntegerOption(options, "timeout-ms", DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
739
- }
740
- async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS, label = "request") {
741
- const controller = new AbortController();
742
- const timer = setTimeout(() => controller.abort(), timeout);
743
- try {
744
- return await fetch(input, { ...init, signal: controller.signal });
745
- }
746
- catch (error) {
747
- if (error instanceof Error && error.name === "AbortError")
748
- throw new Error(`${label} timed out after ${timeout}ms`);
749
- throw error;
750
- }
751
- finally {
752
- clearTimeout(timer);
753
- }
754
- }
755
- function remoteBaseFromCatalogPayload(payload) {
756
- const base = payload.base_url ?? payload.baseUrl;
757
- return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
758
- }
759
- async function remoteBaseFromSource(source, payload, options) {
760
- const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
761
- if (fromPayload)
762
- return fromPayload;
763
- if (source.startsWith("http://") || source.startsWith("https://")) {
764
- return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
765
- }
766
- try {
767
- return remoteBaseFromCatalogPayload(await readCatalogObject(source, options));
768
- }
769
- catch {
770
- return undefined;
771
- }
772
- }
773
- function catalogDetailSource(source, section, name) {
774
- if (source.startsWith("http://") || source.startsWith("https://")) {
775
- const base = new URL(source);
776
- const pathname = base.pathname.endsWith("/catalog.json")
777
- ? base.pathname.slice(0, -"catalog.json".length)
778
- : base.pathname.endsWith("/")
779
- ? base.pathname
780
- : `${base.pathname}/`;
781
- base.pathname = `${pathname}${section}/${providerFilename(name)}`;
782
- base.search = "";
783
- base.hash = "";
784
- return base.toString();
785
- }
786
- const stat = fs.existsSync(source) ? fs.statSync(source) : undefined;
787
- const root = stat?.isDirectory() ? source : path.dirname(source);
788
- const direct = path.join(root, section, `${name}.json`);
789
- if (fs.existsSync(direct))
790
- return direct;
791
- return path.join(root, section, providerFilename(name));
792
- }
793
- async function readCatalogProvider(source, name, options) {
794
- const providers = await readCatalog(source, options);
795
- const summary = providers.find((item) => item.name === name || item.fqn === name);
796
- if (!summary)
797
- throw new Error(`provider not found: ${name}`);
798
- const fqn = summary.fqn ?? summary.name ?? name;
799
- try {
800
- return await readJson(catalogDetailSource(source, "providers", fqn), options);
801
- }
802
- catch {
803
- return summary;
804
- }
805
- }
806
- async function readCatalogPayProvider(source, name, options) {
807
- const providers = await readCatalog(source, options);
808
- const summary = providers.find((item) => item.name === name || item.fqn === name);
809
- const fqn = summary?.fqn ?? summary?.name ?? name;
810
- try {
811
- return await readJson(catalogDetailSource(source, "pay", fqn), options);
812
- }
813
- catch {
814
- if (summary)
815
- return readCatalogProvider(source, name, options);
816
- throw new Error(`provider not found: ${name}`);
817
- }
818
- }
819
- async function cacheProviderAssets(source, catalogPayload, options) {
820
- const base = await remoteBaseFromSource(source, catalogPayload, options);
821
- const warnings = [];
822
- if (!base)
823
- return { detailCount: 0, payCount: 0, warnings };
824
- let detailCount = 0;
825
- let payCount = 0;
826
- for (const provider of catalogPayload.providers ?? []) {
827
- const fqn = provider?.fqn ?? provider?.name;
828
- if (typeof fqn !== "string" || !fqn)
829
- continue;
830
- const filename = providerFilename(fqn);
831
- try {
832
- const detail = await readJson(new URL(`providers/${filename}`, base).toString(), options);
833
- writeJson(path.join(cacheDir(), "providers", filename), detail);
834
- detailCount += 1;
835
- }
836
- catch (error) {
837
- warnings.push(`failed to cache provider detail ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
838
- }
839
- try {
840
- const pay = await readJson(new URL(`pay/${filename}`, base).toString(), options);
841
- writeJson(path.join(cacheDir(), "pay", filename), pay);
842
- payCount += 1;
843
- }
844
- catch (error) {
845
- warnings.push(`failed to cache pay JSON ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
846
- }
847
- }
848
- return { detailCount, payCount, warnings };
849
- }
850
- async function catalogUpdate(source, options) {
851
- let payload;
852
- const warnings = [];
853
- for (let attempt = 1; attempt <= CATALOG_UPDATE_RETRIES; attempt += 1) {
854
- try {
855
- payload = await readCatalogObject(source, options);
856
- break;
857
- }
858
- catch (error) {
859
- const message = error instanceof Error ? error.message : String(error);
860
- if (attempt === CATALOG_UPDATE_RETRIES)
861
- throw error;
862
- warnings.push(`catalog update attempt ${attempt} failed: ${message}`);
863
- await delay(250 * attempt);
864
- }
865
- }
866
- if (!payload)
867
- throw new Error(`failed to read catalog from ${source}`);
868
- writeJson(cachedCatalogPath(), payload);
869
- const cached = await cacheProviderAssets(source, payload, options);
870
- const result = {
871
- source,
872
- path: cachedCatalogPath(),
873
- providerCount: payload.provider_count ?? payload.providerCount ?? (payload.providers ?? []).length,
874
- detailCount: cached.detailCount,
875
- payCount: cached.payCount,
876
- warnings: [...warnings, ...cached.warnings],
877
- };
878
- emit({ command: "catalog update", mode: outputMode(options), result });
879
- }
880
- function zhCopy(title, subtitle, description, useCase) {
881
- return { title, subtitle, description, useCase };
882
- }
883
- function submissionCatalog(detail) {
884
- const title = String(detail.title ?? detail.fqn);
885
- const subtitle = String(detail.subtitle ?? detail.use_case ?? title);
886
- const description = String(detail.description ?? subtitle);
887
- const useCase = String(detail.use_case ?? detail.useCase ?? description);
888
- return {
889
- version: 1,
890
- fqn: detail.fqn,
891
- title,
892
- subtitle,
893
- description,
894
- useCase,
895
- i18n: detail.i18n ?? { "zh-CN": zhCopy(title, subtitle, description, useCase) },
896
- logo: detail.logo ?? "https://x402-catalog.bankofai.io/assets/providers/default.png",
897
- category: detail.category ?? "other",
898
- chains: detail.chains ?? [],
899
- isFirstParty: Boolean(detail.is_first_party ?? detail.isFirstParty),
900
- isFeatured: Boolean(detail.is_featured ?? detail.isFeatured),
901
- featuredTags: detail.featured_tags ?? detail.featuredTags ?? [],
902
- serviceUrl: detail.service_url ?? detail.serviceUrl,
903
- endpoints: (detail.endpoints ?? []).map((endpoint) => {
904
- const endpointTitle = endpoint.title ?? endpoint.path;
905
- const endpointSubtitle = endpoint.subtitle ?? endpoint.path;
906
- const endpointDescription = endpoint.description ?? description;
907
- const endpointUseCase = endpoint.use_case ?? endpoint.useCase ?? useCase;
908
- return {
909
- method: endpoint.method,
910
- path: endpoint.path,
911
- url: endpoint.url,
912
- title: endpointTitle,
913
- subtitle: endpointSubtitle,
914
- description: endpointDescription,
915
- useCase: endpointUseCase,
916
- i18n: endpoint.i18n ?? { "zh-CN": zhCopy(endpointTitle, endpointSubtitle, endpointDescription, endpointUseCase) },
917
- metered: Boolean(endpoint.metered),
918
- minPriceUsd: endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0,
919
- maxPriceUsd: endpoint.max_price_usd ?? endpoint.maxPriceUsd ?? 0,
920
- };
921
- }),
922
- status: detail.status ?? {
923
- catalog: "draft",
924
- gateway: "unknown",
925
- payment: "unknown",
926
- upstream: "unknown",
927
- },
928
- };
929
- }
930
- function payMarkdownFromDetail(detail) {
931
- const lines = [
932
- `# ${detail.title ?? detail.fqn}`,
933
- "",
934
- "## Service",
935
- "",
936
- `- FQN: \`${detail.fqn}\``,
937
- `- Service URL: \`${detail.service_url ?? detail.serviceUrl ?? ""}\``,
938
- `- Category: \`${detail.category ?? ""}\``,
939
- `- Chains: \`${(detail.chains ?? []).join(", ")}\``,
940
- "",
941
- "## Endpoints",
942
- "",
943
- ];
944
- for (const endpoint of detail.endpoints ?? []) {
945
- const metered = Boolean(endpoint.metered);
946
- const price = endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0;
947
- lines.push(`### ${endpoint.method} ${endpoint.path}`, "", endpoint.description ?? "", "", `- URL: \`${endpoint.url ?? ""}\``, `- Metered: \`${String(metered)}\``, `- Price: \`$${price}\``, "");
948
- if (metered) {
949
- lines.push("```bash", `x402-cli pay '${endpoint.url ?? ""}'`, "```", "");
950
- }
951
- else {
952
- lines.push("No payment required.", "");
953
- }
954
- }
955
- lines.push("## Notes", "", "This file is public. Do not include upstream API keys, bearer tokens, provider.yml, `.env`, passwords, or private infrastructure URLs.", "");
956
- return lines.join("\n");
957
- }
958
- async function catalogExportGateway(gatewayUrl, options) {
959
- requireArgument(gatewayUrl, "gateway-url", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
960
- const providerFqn = requireArgument(opt(options, "provider"), "--provider", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
961
- sanitizeProviderName(providerFqn);
962
- const base = gatewayUrl.replace(/\/+$/, "");
963
- const detail = await readJson(`${base}/__402/catalog/providers/${providerFilename(providerFqn)}`, options);
964
- const outputRoot = opt(options, "output-dir", "providers");
965
- const target = opt(options, "output-dir")
966
- ? path.resolve(outputRoot)
967
- : safeOutputPath("providers", providerFilename(providerFqn).replace(/\.json$/, ""));
968
- fs.mkdirSync(target, { recursive: true });
969
- const catalogPath = path.join(target, "catalog.json");
970
- const payMdPath = path.join(target, "pay.md");
971
- ensureWritable(catalogPath, options);
972
- ensureWritable(payMdPath, options);
973
- writeJson(catalogPath, submissionCatalog(detail));
974
- fs.writeFileSync(payMdPath, payMarkdownFromDetail(detail));
975
- emit({ command: "catalog export-gateway", mode: outputMode(options), result: { provider: providerFqn, catalog: catalogPath, payMd: payMdPath } });
976
- }
6
+ import { CliError, hasFlag, opt, optAll, outputMode, parseArgs, requireArgument } from "./args.js";
7
+ import { classify, emit, withSdkStdoutRedirect } from "./output.js";
8
+ import { fetchWithTimeout, readBoundedText, responsePayload, timeoutMs } from "./http-client.js";
9
+ import { startServeDaemon } from "./daemon.js";
10
+ import { getVersion, helpText } from "./help.js";
11
+ import { catalogBuild, catalogPayAssets, gatewayCheck, gatewayScaffold, gatewayStart } from "./gateway-commands.js";
12
+ import { catalogSearch, defaultCatalogSource, handleCatalog } from "./catalog-commands.js";
977
13
  function buildRequirement(options) {
978
14
  const network = normalizeNetwork(opt(options, "network", "tron:0xcd8690dc"));
979
15
  const scheme = opt(options, "scheme", "exact");
@@ -994,8 +30,8 @@ function buildRequirement(options) {
994
30
  if (!explicitAsset && !registryToken)
995
31
  throw new Error(`unknown token ${tokenSymbol} on ${network}`);
996
32
  const decimals = decimalsOption !== undefined ? Number(decimalsOption) : registryToken.decimals;
997
- if (!Number.isInteger(decimals) || decimals < 0)
998
- throw new Error("--decimals must be a non-negative integer");
33
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
34
+ throw new Error("--decimals must be an integer between 0 and 255");
999
35
  const rawAmount = opt(options, "rawAmount") ?? opt(options, "raw-amount");
1000
36
  const humanAmount = opt(options, "amount");
1001
37
  if (rawAmount && humanAmount)
@@ -1003,13 +39,17 @@ function buildRequirement(options) {
1003
39
  const amount = rawAmount ? assertRawAmount(rawAmount, "--raw-amount") : toSmallestUnit(humanAmount ?? "0.0001", decimals);
1004
40
  const assetAddress = explicitAsset ?? registryToken.address;
1005
41
  const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
42
+ const maxTimeoutSeconds = Number(opt(options, "valid-for-seconds", "300"));
43
+ if (!Number.isInteger(maxTimeoutSeconds) || maxTimeoutSeconds <= 0 || maxTimeoutSeconds > 86400) {
44
+ throw new Error("--valid-for-seconds must be an integer between 1 and 86400");
45
+ }
1006
46
  return {
1007
47
  scheme,
1008
48
  network,
1009
49
  amount,
1010
50
  asset: assetAddress,
1011
51
  payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
1012
- maxTimeoutSeconds: Number(opt(options, "valid-for-seconds", "300")),
52
+ maxTimeoutSeconds,
1013
53
  extra: scheme === "exact" && assetTransferMethod ? { assetTransferMethod } : {},
1014
54
  };
1015
55
  }
@@ -1019,10 +59,16 @@ async function facilitatorPost(baseUrl, path, body, options) {
1019
59
  headers: { "content-type": "application/json" },
1020
60
  body: JSON.stringify(body),
1021
61
  }, timeoutMs(options), `facilitator ${path}`);
1022
- const text = await response.text();
1023
- const data = text ? JSON.parse(text) : {};
62
+ const text = await readBoundedText(response, `facilitator ${path} response`);
63
+ let data = {};
64
+ try {
65
+ data = text ? JSON.parse(text) : {};
66
+ }
67
+ catch {
68
+ throw new Error(`facilitator ${path} returned invalid JSON`);
69
+ }
1024
70
  if (!response.ok)
1025
- throw new Error(`facilitator ${path} failed: ${response.status} ${text}`);
71
+ throw new Error(`facilitator ${path} failed with HTTP ${response.status}`);
1026
72
  return data;
1027
73
  }
1028
74
  function requestHeaders(options) {
@@ -1035,37 +81,6 @@ function requestHeaders(options) {
1035
81
  }
1036
82
  return headersOut;
1037
83
  }
1038
- function stripFlag(argv, flag) {
1039
- return argv.filter(item => item !== flag);
1040
- }
1041
- function serveDaemon(argv, options) {
1042
- const requirement = buildRequirement(options);
1043
- if (!requirement.payTo)
1044
- throw new Error("--pay-to is required");
1045
- const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
1046
- const script = fileURLToPath(import.meta.url);
1047
- const child = spawn(process.execPath, [script, ...daemonArgs], {
1048
- detached: true,
1049
- stdio: "ignore",
1050
- env: process.env,
1051
- });
1052
- child.unref();
1053
- const host = opt(options, "host", "127.0.0.1");
1054
- const port = Number(opt(options, "port", "4020"));
1055
- const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
1056
- emit({
1057
- command: "server",
1058
- mode: outputMode(options),
1059
- network: requirement.network,
1060
- scheme: requirement.scheme,
1061
- result: {
1062
- pid: child.pid,
1063
- pay_url: resourceUrl,
1064
- resource_url: resourceUrl,
1065
- daemon: true,
1066
- },
1067
- });
1068
- }
1069
84
  function validateAmountLimits(selected, options) {
1070
85
  const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
1071
86
  const maxAmount = opt(options, "max-amount");
@@ -1079,8 +94,8 @@ function validateAmountLimits(selected, options) {
1079
94
  throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-raw-amount or --decimals");
1080
95
  }
1081
96
  const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
1082
- if (!Number.isInteger(decimals) || decimals < 0)
1083
- throw new Error("--decimals must be a non-negative integer");
97
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
98
+ throw new Error("--decimals must be an integer between 0 and 255");
1084
99
  if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, decimals))) {
1085
100
  throw new Error(`payment amount exceeds --max-amount ${maxAmount}`);
1086
101
  }
@@ -1107,8 +122,8 @@ function gasfreeFeeLimitRaw(selected, options) {
1107
122
  throw new Error("cannot evaluate --max-gasfree-fee for an unknown asset; pass --max-gasfree-fee-raw or --decimals");
1108
123
  }
1109
124
  const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
1110
- if (!Number.isInteger(decimals) || decimals < 0)
1111
- throw new Error("--decimals must be a non-negative integer");
125
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
126
+ throw new Error("--decimals must be an integer between 0 and 255");
1112
127
  return toSmallestUnit(maxHuman, decimals);
1113
128
  }
1114
129
  async function serve(options) {
@@ -1173,7 +188,7 @@ async function serve(options) {
1173
188
  paymentPayload: payload,
1174
189
  paymentRequirements: requirement,
1175
190
  }, options);
1176
- if (!(settle?.success === true || settle?.settled === true || typeof settle?.transaction === "string" || typeof settle?.txHash === "string")) {
191
+ if (!(settle?.success === true && typeof settle?.transaction === "string" && settle.transaction.length > 0 && typeof settle?.network === "string" && settle.network.length > 0)) {
1177
192
  response.writeHead(502, { "content-type": "application/json" });
1178
193
  response.end(JSON.stringify({ error: "settlement failed" }));
1179
194
  return;
@@ -1182,7 +197,7 @@ async function serve(options) {
1182
197
  "content-type": "application/json",
1183
198
  [headers.response]: encodeResponse(settle),
1184
199
  });
1185
- response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction ?? settle.txHash ?? null }));
200
+ response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction }));
1186
201
  }
1187
202
  catch (error) {
1188
203
  response.writeHead(500, { "content-type": "application/json" });
@@ -1209,6 +224,17 @@ function selectRequirement(accepts, options) {
1209
224
  const scheme = opt(options, "scheme");
1210
225
  const token = opt(options, "token");
1211
226
  const selected = accepts.find(req => {
227
+ if (!req || !["exact", "exact_gasfree"].includes(req.scheme) || typeof req.network !== "string" || typeof req.asset !== "string")
228
+ return false;
229
+ if (req.scheme === "exact_gasfree" && !req.network.startsWith("tron:"))
230
+ return false;
231
+ try {
232
+ if (!findTokenByAddress(req.network, req.asset))
233
+ return false;
234
+ }
235
+ catch {
236
+ return false;
237
+ }
1212
238
  if (network && normalizeNetwork(network) !== req.network)
1213
239
  return false;
1214
240
  if (scheme && scheme !== req.scheme)
@@ -1233,6 +259,9 @@ function selectRequirement(accepts, options) {
1233
259
  async function pay(url, options) {
1234
260
  requireArgument(url, "URL", "x402-cli pay <url> [options]");
1235
261
  const method = opt(options, "method", "GET");
262
+ if (!/^[A-Z]+$/.test(method) || !["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"].includes(method)) {
263
+ throw new CliError("INVALID_ARGUMENT", `unsupported HTTP method ${method}`, "Use an uppercase standard HTTP method.", 2);
264
+ }
1236
265
  const baseHeaders = requestHeaders(options);
1237
266
  const probe = await fetchWithTimeout(url, {
1238
267
  method,
@@ -1298,7 +327,7 @@ async function pay(url, options) {
1298
327
  const body = await responsePayload(paid);
1299
328
  const paymentResponse = paid.headers.get(headers.response);
1300
329
  const settlement = paymentResponse ? decodeResponse(paymentResponse) : undefined;
1301
- const settled = settlement !== undefined;
330
+ const settled = settlement?.success === true && typeof settlement?.transaction === "string" && settlement.transaction.length > 0 && typeof settlement?.network === "string" && settlement.network.length > 0;
1302
331
  const result = {
1303
332
  url,
1304
333
  status: paid.status,
@@ -1312,6 +341,9 @@ async function pay(url, options) {
1312
341
  } : {}),
1313
342
  ...(creation.gasfreeEstimate ? { gasfreeEstimate: creation.gasfreeEstimate } : {}),
1314
343
  };
344
+ if (paymentResponse && !settled) {
345
+ throw new CliError("INVALID_SETTLEMENT", "gateway returned an invalid or unsuccessful PAYMENT-RESPONSE", "Do not treat this request as paid; contact the gateway operator.", 1, result);
346
+ }
1315
347
  if (!paid.ok) {
1316
348
  const retryAfter = paid.headers.get("retry-after");
1317
349
  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
@@ -1332,339 +364,9 @@ async function roundtrip(options) {
1332
364
  await pay(`http://127.0.0.1:${port}/pay`, options);
1333
365
  process.exit(0);
1334
366
  }
1335
- function executableInPath(name) {
1336
- for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
1337
- if (!dir)
1338
- continue;
1339
- const candidate = path.join(dir, name);
1340
- try {
1341
- fs.accessSync(candidate, fs.constants.X_OK);
1342
- return candidate;
1343
- }
1344
- catch {
1345
- // Try the next PATH entry.
1346
- }
1347
- }
1348
- return undefined;
1349
- }
1350
- function resolveGatewayPackageRuntime() {
1351
- try {
1352
- return require.resolve("@bankofai/x402-gateway/dist/cli.js");
1353
- }
1354
- catch {
1355
- return undefined;
1356
- }
1357
- }
1358
- function gatewayCommand(options) {
1359
- const explicit = opt(options, "gateway-bin");
1360
- const gatewayPackageRuntime = resolveGatewayPackageRuntime();
1361
- const candidates = [
1362
- explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
1363
- gatewayPackageRuntime ? { file: gatewayPackageRuntime, source: "@bankofai/x402-gateway dependency" } : undefined,
1364
- { file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
1365
- executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
1366
- { file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
1367
- ].filter(Boolean);
1368
- for (const candidate of candidates) {
1369
- try {
1370
- fs.accessSync(candidate.file, fs.constants.R_OK);
1371
- if (candidate.file.endsWith(".js")) {
1372
- return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
1373
- }
1374
- return { command: candidate.file, argsPrefix: [], source: candidate.source };
1375
- }
1376
- catch {
1377
- // Try the next candidate.
1378
- }
1379
- }
1380
- throw new Error("x402-gateway runtime not found. Install @bankofai/x402-gateway, run from a checkout with ../x402-gateway/dist/cli.js, or pass --gateway-bin <path>.");
1381
- }
1382
- async function gatewayStart(args, options) {
1383
- const gateway = gatewayCommand(options);
1384
- const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
1385
- stdio: "inherit",
1386
- env: process.env,
1387
- });
1388
- await new Promise((resolve, reject) => {
1389
- child.on("exit", code => code === 0 ? resolve() : reject(new Error(`gateway exited with ${code}`)));
1390
- child.on("error", reject);
1391
- });
1392
- }
1393
- function gatewayCheck(target, options) {
1394
- const files = providerFiles(target);
1395
- const providers = files.map(loadProviderFile);
1396
- const names = new Set();
1397
- for (const provider of providers) {
1398
- if (names.has(provider.name))
1399
- throw new Error(`duplicate provider name: ${provider.name}`);
1400
- names.add(provider.name);
1401
- }
1402
- emit({
1403
- command: "gateway check",
1404
- mode: outputMode(options),
1405
- result: { providers: providers.map(p => p.name), count: providers.length },
1406
- });
1407
- }
1408
- function gatewayScaffold(name, options) {
1409
- const outputDir = opt(options, "output-dir", path.join("providers", name));
1410
- const forwardUrl = opt(options, "forward-url", "https://api.example.com");
1411
- fs.mkdirSync(outputDir, { recursive: true });
1412
- const body = `name: ${name}
1413
- title: "${name}"
1414
- description: "x402 provider"
1415
- category: data
1416
- version: v1
1417
-
1418
- forward_url: ${forwardUrl}
1419
-
1420
- routing:
1421
- type: proxy
1422
-
1423
- operator:
1424
- network: tron:0xcd8690dc
1425
- currencies:
1426
- usd: ["USDT"]
1427
- recipient: <provider-recipient-address>
1428
- scheme: exact
1429
- protocol: exact
1430
- asset_transfer_method: permit2
1431
- facilitator_url: https://facilitator.bankofai.io
1432
- facilitator_api_key: <facilitator-api-key>
1433
- valid_for_seconds: 300
1434
-
1435
- endpoints:
1436
- - method: GET
1437
- path: /v1/ping
1438
- metering:
1439
- dimensions:
1440
- - tiers:
1441
- - price_usd: 0.0001
1442
- `;
1443
- fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
1444
- emit({
1445
- command: "gateway scaffold",
1446
- mode: outputMode(options),
1447
- result: { file: path.join(outputDir, "provider.yml") },
1448
- });
1449
- }
1450
- function catalogBuild(target, options) {
1451
- const providers = providerFiles(target).map(loadProviderFile).map(providerCatalog);
1452
- const catalog = { version: 1, generatedAt: new Date().toISOString(), providers };
1453
- const output = opt(options, "output", opt(options, "dist-dir") ? path.join(opt(options, "dist-dir"), "catalog.json") : undefined);
1454
- if (output) {
1455
- fs.mkdirSync(path.dirname(output), { recursive: true });
1456
- fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
1457
- emit({
1458
- command: "catalog build",
1459
- mode: outputMode(options),
1460
- result: { output, count: providers.length },
1461
- });
1462
- }
1463
- else if (outputMode(options) === "json") {
1464
- emit({ command: "catalog build", mode: "json", result: catalog });
1465
- }
1466
- else {
1467
- printJson(catalog);
1468
- }
1469
- }
1470
- function catalogPayAssets(target, options) {
1471
- const rows = providerFiles(target).map(loadProviderFile).flatMap(provider => (provider.endpoints ?? []).map((endpoint) => ({
1472
- provider: provider.name,
1473
- method: endpoint.method,
1474
- path: `/providers/${provider.name}${endpoint.path}`,
1475
- network: normalizeNetwork(provider.operator.network),
1476
- currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
1477
- price_usd: providerPrice(endpoint),
1478
- scheme: providerScheme(provider),
1479
- assetTransferMethod: providerAssetTransferMethod(provider),
1480
- })));
1481
- emit({
1482
- command: "gateway catalog pay-assets",
1483
- mode: outputMode(options),
1484
- result: { count: rows.length, assets: rows },
1485
- });
1486
- }
1487
- async function readProviderDetailForSearch(source, fqn, options) {
1488
- try {
1489
- return await readJson(catalogDetailSource(source, "providers", fqn), options);
1490
- }
1491
- catch {
1492
- return {};
1493
- }
1494
- }
1495
- async function searchCatalog(source, query, options) {
1496
- const providers = await readCatalog(source, options);
1497
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1498
- if (!terms.length)
1499
- return [];
1500
- const includeBlocked = hasFlag(options, "include-blocked");
1501
- const hits = [];
1502
- for (const provider of providers) {
1503
- if (provider.block && !includeBlocked)
1504
- continue;
1505
- const fqn = String(provider.fqn ?? provider.name ?? "");
1506
- if (!fqn)
1507
- continue;
1508
- const detail = await readProviderDetailForSearch(source, fqn, options);
1509
- const tags = stringList(detail.featured_tags ?? provider.featured_tags ?? detail.tags ?? provider.tags);
1510
- const endpoints = Array.isArray(detail.endpoints) ? detail.endpoints : Array.isArray(provider.endpoints) ? provider.endpoints : [];
1511
- const categoryMeta = detail.category_meta ?? provider.category_meta;
1512
- const chains = stringList(detail.chains ?? provider.chains);
1513
- const chainKinds = stringList(detail.chain_kinds ?? provider.chain_kinds);
1514
- const chainsMetaRaw = detail.chains_meta ?? provider.chains_meta ?? [];
1515
- const chainsMeta = Array.isArray(chainsMetaRaw) ? chainsMetaRaw.filter(item => item && typeof item === "object") : [];
1516
- const titleZh = String(detail.title_zh ?? provider.title_zh ?? "");
1517
- const mainTitle = String(detail.main_title ?? provider.main_title ?? detail.mainTitle ?? provider.mainTitle ?? "");
1518
- const subTitle = String(detail.sub_title ?? provider.sub_title ?? detail.subTitle ?? provider.subTitle ?? "");
1519
- const fields = {
1520
- fqn: [fqn],
1521
- title: [String(detail.title ?? provider.title ?? ""), mainTitle],
1522
- i18n: [titleZh, subTitle, ...dictValues(detail.i18n ?? provider.i18n)],
1523
- category: [String(detail.category ?? provider.category ?? "")],
1524
- category_meta: dictValues(categoryMeta),
1525
- chains: [...chains, ...chainMetaValues(chainsMeta)],
1526
- chain_kinds: chainKinds,
1527
- service_url: [String(detail.service_url ?? provider.service_url ?? detail.serviceUrl ?? provider.serviceUrl ?? "")],
1528
- description: [String(detail.description ?? provider.description ?? "")],
1529
- use_case: [String(detail.use_case ?? provider.use_case ?? detail.useCase ?? provider.useCase ?? "")],
1530
- tags,
1531
- endpoints: endpointFields(endpoints),
1532
- };
1533
- const scored = scoreFields(terms, fields);
1534
- if (scored.score === 0)
1535
- continue;
1536
- hits.push({
1537
- provider,
1538
- detail,
1539
- fqn,
1540
- title: fields.title[0],
1541
- category: fields.category[0],
1542
- serviceUrl: fields.service_url[0],
1543
- description: fields.description[0] || undefined,
1544
- useCase: fields.use_case[0] || undefined,
1545
- titleZh: titleZh || undefined,
1546
- mainTitle: mainTitle || undefined,
1547
- subTitle: subTitle || undefined,
1548
- categoryMeta: categoryMeta && typeof categoryMeta === "object" ? categoryMeta : undefined,
1549
- chains,
1550
- chainKinds,
1551
- chainsMeta,
1552
- tags,
1553
- endpoints,
1554
- score: scored.score,
1555
- matchedFields: scored.matchedFields,
1556
- });
1557
- }
1558
- return hits
1559
- .sort((a, b) => b.score - a.score || a.fqn.localeCompare(b.fqn))
1560
- .slice(0, positiveIntegerOption(options, "limit", 10));
1561
- }
1562
- function searchHitToJson(hit) {
1563
- return {
1564
- fqn: hit.fqn,
1565
- title: hit.title,
1566
- category: hit.category,
1567
- serviceUrl: hit.serviceUrl,
1568
- description: hit.description,
1569
- useCase: hit.useCase,
1570
- title_zh: hit.titleZh,
1571
- main_title: hit.mainTitle,
1572
- sub_title: hit.subTitle,
1573
- category_meta: hit.categoryMeta,
1574
- chains: hit.chains,
1575
- chain_kinds: hit.chainKinds,
1576
- chains_meta: hit.chainsMeta,
1577
- tags: hit.tags,
1578
- score: hit.score,
1579
- matchedFields: hit.matchedFields,
1580
- endpoints: hit.endpoints,
1581
- };
1582
- }
1583
- async function catalogSearch(source, query, options) {
1584
- positiveIntegerOption(options, "limit", 10);
1585
- const hits = await searchCatalog(source, query, options);
1586
- const results = hits.map(searchHitToJson);
1587
- if (outputMode(options) === "json") {
1588
- emit({ command: "catalog search", mode: "json", result: { query, catalog: source, count: hits.length, results } });
1589
- return;
1590
- }
1591
- if (!hits.length) {
1592
- process.stdout.write("no matches\n");
1593
- return;
1594
- }
1595
- for (const hit of hits) {
1596
- const tags = hit.tags.length ? hit.tags.join(",") : "-";
1597
- process.stdout.write(`${hit.fqn.padEnd(32)} score=${String(hit.score).padEnd(3)} category=${hit.category.padEnd(12)} tags=${tags}\n`);
1598
- if (hit.title)
1599
- process.stdout.write(` ${hit.title}\n`);
1600
- if (hit.description)
1601
- process.stdout.write(` ${hit.description.split("\n")[0]}\n`);
1602
- if (hit.serviceUrl)
1603
- process.stdout.write(` service: ${hit.serviceUrl}\n`);
1604
- for (const endpoint of hit.endpoints.slice(0, 3)) {
1605
- const method = String(endpoint.method ?? "");
1606
- const pathText = String(endpoint.path ?? endpoint.url ?? "");
1607
- const paid = endpoint.paid;
1608
- let suffix = "";
1609
- if (paid && typeof paid === "object") {
1610
- suffix = ` ${paid.network ?? ""} ${paid.currency ?? ""} ${paid.amount_raw ?? ""}`.trimEnd();
1611
- }
1612
- process.stdout.write(` ${method.padEnd(6)} ${pathText}${suffix ? ` ${suffix}` : ""}\n`);
1613
- }
1614
- process.stdout.write("\n");
1615
- }
1616
- }
1617
- async function catalogShow(source, name, options) {
1618
- requireArgument(name, "provider", "x402-cli catalog show <provider> [--catalog <source>]");
1619
- const provider = await readCatalogProvider(source, name, options);
1620
- if (outputMode(options) === "json") {
1621
- emit({ command: "catalog show", mode: "json", result: provider });
1622
- return;
1623
- }
1624
- process.stdout.write(`${provider.fqn ?? provider.name} - ${provider.title ?? provider.main_title ?? provider.name}\n`);
1625
- if (provider.description)
1626
- process.stdout.write(`${String(provider.description).split("\n")[0]}\n`);
1627
- if (provider.category)
1628
- process.stdout.write(`category: ${provider.category}\n`);
1629
- if (provider.chains)
1630
- process.stdout.write(`chains: ${provider.chains.join(", ")}\n`);
1631
- }
1632
- async function catalogEndpoints(source, name, options) {
1633
- requireArgument(name, "provider", "x402-cli catalog endpoints <provider> [--catalog <source>]");
1634
- const provider = await readCatalogProvider(source, name, options);
1635
- const endpoints = provider.endpoints ?? [];
1636
- if (outputMode(options) === "json") {
1637
- emit({ command: "catalog endpoints", mode: "json", result: { provider: provider.fqn ?? provider.name, endpoints } });
1638
- return;
1639
- }
1640
- for (const endpoint of endpoints) {
1641
- process.stdout.write(`${String(endpoint.method ?? "").padEnd(6)} ${endpoint.path ?? endpoint.url ?? ""}\n`);
1642
- if (endpoint.description)
1643
- process.stdout.write(` ${String(endpoint.description).split("\n")[0]}\n`);
1644
- }
1645
- }
1646
- async function catalogPayJson(source, name, options) {
1647
- requireArgument(name, "provider", "x402-cli catalog pay-json <provider> [--catalog <source>]");
1648
- const provider = await readCatalogPayProvider(source, name, options);
1649
- const endpoint = (provider.endpoints ?? []).find((item) => item.paid || item.x402_routes?.length || item.x402Routes?.length) ?? provider.endpoints?.[0];
1650
- if (!endpoint)
1651
- throw new Error(`provider has no endpoints: ${name}`);
1652
- const result = {
1653
- provider: provider.fqn ?? provider.name,
1654
- url: endpoint.url ?? endpoint.path,
1655
- method: endpoint.method,
1656
- paid: endpoint.paid,
1657
- x402_routes: endpoint.x402_routes ?? endpoint.x402Routes ?? [],
1658
- endpoint,
1659
- };
1660
- if (hasFlag(options, "raw"))
1661
- printJson(result);
1662
- else
1663
- emit({ command: "catalog pay-json", mode: outputMode(options), result });
1664
- }
1665
367
  async function handleGateway(args) {
1666
368
  const { command, positional, options } = parseArgs(args);
1667
- if (hasFlag(options, "help") || command === "help") {
369
+ if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
1668
370
  process.stdout.write(helpText(command === "catalog" || positional[0] === "catalog" ? "gateway-catalog" : "gateway"));
1669
371
  return;
1670
372
  }
@@ -1695,31 +397,6 @@ async function handleGatewayCatalog(positional, options) {
1695
397
  else
1696
398
  throw new CliError("UNKNOWN_COMMAND", `Unknown gateway catalog command: ${sub}`, "Run x402-cli gateway catalog --help to list commands.", 2);
1697
399
  }
1698
- async function handleCatalog(args) {
1699
- const { command, positional, options } = parseArgs(args);
1700
- if (hasFlag(options, "help") || command === "help") {
1701
- const topic = command === "help" ? positional[0] : command;
1702
- process.stdout.write(helpText(topic ? `catalog-${topic}` : "catalog"));
1703
- return;
1704
- }
1705
- const source = opt(options, "catalog", defaultCatalogSource());
1706
- if (command === "update")
1707
- await catalogUpdate(source, options);
1708
- else if (command === "search")
1709
- await catalogSearch(source, requireArgument(positional.join(" "), "query", "x402-cli catalog search <query> [options]"), options);
1710
- else if (command === "show")
1711
- await catalogShow(source, positional[0], options);
1712
- else if (command === "endpoints")
1713
- await catalogEndpoints(source, positional[0], options);
1714
- else if (command === "pay-json")
1715
- await catalogPayJson(source, positional[0], options);
1716
- else if (command === "export-gateway")
1717
- await catalogExportGateway(positional[0], options);
1718
- else if (command === "build")
1719
- catalogBuild(positional[0] ?? "providers", options);
1720
- else
1721
- throw new CliError("UNKNOWN_COMMAND", `Unknown catalog command: ${command}`, "Run x402-cli catalog --help to list commands.", 2);
1722
- }
1723
400
  async function main() {
1724
401
  const argv = process.argv.slice(2);
1725
402
  const { command, positional, options } = parseArgs(argv);
@@ -1742,7 +419,7 @@ async function main() {
1742
419
  }
1743
420
  if (command === "serve") {
1744
421
  if (hasFlag(options, "daemon"))
1745
- serveDaemon(argv, options);
422
+ await startServeDaemon(argv, options, buildRequirement(options), fileURLToPath(import.meta.url));
1746
423
  else
1747
424
  await serve(options);
1748
425
  }