@bankofai/x402-cli 1.0.1-beta.0 → 1.0.1-beta.10

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