@bankofai/x402-cli 1.0.0-beta.0 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -2
  2. package/dist/cli.js +907 -82
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -29,6 +29,18 @@ Run the compiled CLI:
29
29
  node dist/cli.js <command> [options]
30
30
  ```
31
31
 
32
+ Common CLI options:
33
+
34
+ ```bash
35
+ x402-cli --help
36
+ x402-cli --version
37
+ x402-cli pay --help
38
+ ```
39
+
40
+ Output is human-readable by default. Add `--json` to commands such as `pay`,
41
+ `serve`, `gateway check`, and `catalog search` for a stable machine-readable
42
+ envelope with `ok`, `command`, `result`, or structured `error` fields.
43
+
32
44
  ## Commands
33
45
 
34
46
  ### Serve
@@ -96,10 +108,10 @@ Aliases accepted:
96
108
 
97
109
  ## Facilitator
98
110
 
99
- Set a facilitator URL when needed:
111
+ Pass a facilitator URL when needed:
100
112
 
101
113
  ```bash
102
- FACILITATOR_URL=https://facilitator.bankofai.io
114
+ x402-cli serve --facilitator-url https://facilitator.bankofai.io ...
103
115
  ```
104
116
 
105
117
  CLI payment challenges and payload selection always emit `scheme: "exact"` for
package/dist/cli.js CHANGED
@@ -2,24 +2,58 @@
2
2
  import http from "node:http";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
4
  import fs from "node:fs";
5
+ import os from "node:os";
5
6
  import path from "node:path";
6
7
  import { spawn } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
7
9
  import YAML from "yaml";
8
10
  import { createPaymentPayload, decodeRequired, decodeResponse, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
9
- import { getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
11
+ import { findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
12
+ const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "help", "human", "include-blocked", "json", "version"]);
10
13
  function parseArgs(argv) {
11
14
  const [command = "help", ...rest] = argv;
12
15
  const positional = [];
13
16
  const options = {};
14
17
  for (let i = 0; i < rest.length; i += 1) {
15
18
  const item = rest[i];
19
+ if (item === "-h") {
20
+ options.help = true;
21
+ continue;
22
+ }
23
+ if (item === "-V") {
24
+ options.version = true;
25
+ continue;
26
+ }
27
+ if (item === "-d") {
28
+ options.daemon = true;
29
+ continue;
30
+ }
31
+ if (item === "-n") {
32
+ const next = rest[i + 1];
33
+ if (!next || next.startsWith("-")) {
34
+ options.limit = true;
35
+ }
36
+ else {
37
+ options.limit = next;
38
+ i += 1;
39
+ }
40
+ continue;
41
+ }
16
42
  if (!item.startsWith("--")) {
17
43
  positional.push(item);
18
44
  continue;
19
45
  }
20
- const key = item.slice(2);
46
+ const eq = item.indexOf("=");
47
+ const key = eq > 2 ? item.slice(2, eq) : item.slice(2);
48
+ const inline = eq > 2 ? item.slice(eq + 1) : undefined;
21
49
  const next = rest[i + 1];
22
- if (!next || next.startsWith("--")) {
50
+ if (inline !== undefined) {
51
+ options[key] = inline;
52
+ }
53
+ else if (BOOLEAN_FLAGS.has(key)) {
54
+ options[key] = true;
55
+ }
56
+ else if (!next || next.startsWith("-")) {
23
57
  options[key] = true;
24
58
  }
25
59
  else {
@@ -35,10 +69,25 @@ function parseArgs(argv) {
35
69
  }
36
70
  return { command, positional, options };
37
71
  }
72
+ function getVersion() {
73
+ try {
74
+ const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
75
+ return String(pkg.version ?? "0.0.0");
76
+ }
77
+ catch {
78
+ return "0.0.0";
79
+ }
80
+ }
38
81
  function opt(options, key, fallback) {
39
82
  const value = options[key];
40
83
  return typeof value === "string" ? value : fallback;
41
84
  }
85
+ function hasFlag(options, key) {
86
+ return options[key] === true;
87
+ }
88
+ function outputMode(options) {
89
+ return hasFlag(options, "json") ? "json" : "human";
90
+ }
42
91
  function optAll(options, key) {
43
92
  const value = options[key];
44
93
  if (Array.isArray(value))
@@ -48,6 +97,247 @@ function optAll(options, key) {
48
97
  function printJson(value) {
49
98
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
50
99
  }
100
+ function emit(args) {
101
+ const mode = args.mode ?? "human";
102
+ if (mode === "json") {
103
+ const envelope = {
104
+ ok: !args.error,
105
+ command: args.command,
106
+ };
107
+ if (args.network)
108
+ envelope.network = args.network;
109
+ if (args.scheme)
110
+ envelope.scheme = args.scheme;
111
+ if (args.error)
112
+ envelope.error = args.error;
113
+ else
114
+ envelope.result = args.result ?? null;
115
+ printJson(envelope);
116
+ return;
117
+ }
118
+ if (args.error) {
119
+ process.stderr.write(`ERROR ${args.command}: ${args.error.code}\n`);
120
+ process.stderr.write(` ${args.error.message}\n`);
121
+ if (args.error.hint)
122
+ process.stderr.write(` hint: ${args.error.hint}\n`);
123
+ return;
124
+ }
125
+ const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
126
+ process.stdout.write(`OK ${args.command}${suffix ? ` (${suffix})` : ""}\n`);
127
+ if (args.result && typeof args.result === "object" && !Array.isArray(args.result)) {
128
+ for (const [key, value] of Object.entries(args.result)) {
129
+ if (value === undefined)
130
+ continue;
131
+ if (value && typeof value === "object") {
132
+ process.stdout.write(` ${key}: ${JSON.stringify(value)}\n`);
133
+ }
134
+ else {
135
+ process.stdout.write(` ${key}: ${value}\n`);
136
+ }
137
+ }
138
+ }
139
+ else if (args.result !== undefined) {
140
+ process.stdout.write(` ${args.result}\n`);
141
+ }
142
+ }
143
+ function classify(error) {
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ const lower = message.toLowerCase();
146
+ if (lower.includes("missing private key") || lower.includes("could not find a wallet")) {
147
+ return {
148
+ code: "WALLET_NOT_CONFIGURED",
149
+ message,
150
+ hint: "Set PRIVATE_KEY, TRON_PRIVATE_KEY, EVM_PRIVATE_KEY, or configure agent-wallet with a payer wallet.",
151
+ };
152
+ }
153
+ if (lower.includes("wallets_config") || lower.includes("wallet config")) {
154
+ return {
155
+ code: "WALLET_CONFIG_CORRUPT",
156
+ message,
157
+ hint: "Check ~/.agent-wallet/wallets_config.json or recreate the local agent-wallet configuration.",
158
+ };
159
+ }
160
+ if (lower.includes("does not exist") && lower.includes("account [t")) {
161
+ return {
162
+ code: "TRON_ACCOUNT_NOT_ACTIVATED",
163
+ message,
164
+ hint: "Activate the TRON address by sending it a small amount of TRX before signing contract calls.",
165
+ };
166
+ }
167
+ if (lower.includes("permit2_insufficient_balance") || lower.includes("insufficient") && lower.includes("balance")) {
168
+ return {
169
+ code: "INSUFFICIENT_TOKEN_BALANCE",
170
+ message,
171
+ hint: "Fund the payer address with the exact token and network advertised by the provider, then retry.",
172
+ };
173
+ }
174
+ if (lower.includes("transfer_from_failed") || lower.includes("transferfrom failed")) {
175
+ return {
176
+ code: "TOKEN_TRANSFER_FAILED",
177
+ message,
178
+ hint: "Check token balance, token contract, payer address, and that the selected x402 route matches the provider requirement.",
179
+ };
180
+ }
181
+ if (lower.includes("insufficient funds for gas") || lower.includes("insufficient gas") || lower.includes("energy")) {
182
+ return {
183
+ code: "INSUFFICIENT_GAS",
184
+ message,
185
+ hint: "Fund the payer address with the native gas token for this network.",
186
+ };
187
+ }
188
+ if (lower.includes("deadline") || lower.includes("expired")) {
189
+ return {
190
+ code: "DEADLINE_OR_CLOCK_SKEW",
191
+ message,
192
+ hint: "Check local clock sync and retry with a fresh payment requirement.",
193
+ };
194
+ }
195
+ if (lower.includes("permittransferfrom") || lower.includes("invalid signature") || lower.includes("permit reverted")) {
196
+ return {
197
+ code: "PERMIT_REVERTED",
198
+ message,
199
+ hint: "The token or Permit2 contract rejected the signature; retry with a fresh requirement and verify token/network support.",
200
+ };
201
+ }
202
+ if (lower.includes("tokenregistry") && lower.includes("import")) {
203
+ return {
204
+ code: "SDK_API_DRIFT",
205
+ message,
206
+ hint: "Installed x402 SDK packages do not match this CLI; reinstall @bankofai/x402-cli and SDK dependencies.",
207
+ };
208
+ }
209
+ if (lower.includes("429") || lower.includes("too many requests") || lower.includes("rate limit")) {
210
+ return {
211
+ code: "RATE_LIMITED",
212
+ message,
213
+ hint: "Wait briefly and retry; the upstream service or RPC is rate limiting requests.",
214
+ };
215
+ }
216
+ if (lower.includes("402 response missing")) {
217
+ return {
218
+ code: "INVALID_X402_RESPONSE",
219
+ message,
220
+ hint: "The endpoint returned HTTP 402 without a PAYMENT-REQUIRED header.",
221
+ };
222
+ }
223
+ if (lower.includes("no matching payment requirement")) {
224
+ return {
225
+ code: "NO_MATCHING_PAYMENT_REQUIREMENT",
226
+ message,
227
+ hint: "Relax --network, --token, or --scheme, or use values offered by the provider.",
228
+ };
229
+ }
230
+ if (lower.includes("exceeds --max")) {
231
+ return {
232
+ code: "PAYMENT_AMOUNT_TOO_HIGH",
233
+ message,
234
+ hint: "Increase the max amount flag only if this provider price is expected.",
235
+ };
236
+ }
237
+ if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused")) {
238
+ return {
239
+ code: "NETWORK_ERROR",
240
+ message,
241
+ hint: "Check the URL, local server, proxy, and network connectivity.",
242
+ };
243
+ }
244
+ return {
245
+ code: "IO_ERROR",
246
+ message,
247
+ hint: "Run with --json for structured output, and check the provider/gateway logs for details.",
248
+ };
249
+ }
250
+ function helpText(topic = "root") {
251
+ const sections = {
252
+ root: `x402-cli ${getVersion()}
253
+
254
+ Usage:
255
+ x402-cli <command> [options]
256
+
257
+ Commands:
258
+ pay <url> Pay an x402-protected URL
259
+ serve Run a local x402 paywall endpoint
260
+ roundtrip Start serve, pay it, then exit
261
+ gateway <command> Manage local gateway provider files
262
+ catalog <command> Search, cache, and export provider catalog assets
263
+
264
+ Global options:
265
+ -h, --help Show help
266
+ -V, --version Show version
267
+ --json Print machine-readable JSON envelope
268
+ --human Print human-readable output (default)
269
+ `,
270
+ pay: `Usage:
271
+ x402-cli pay <url> [options]
272
+
273
+ Options:
274
+ --method <method> HTTP method (default: GET)
275
+ --header "Name: Value" Request header, repeatable
276
+ --body <body> Request body for non-GET methods
277
+ --network <caip2> Require a specific network
278
+ --token <symbol> Require a specific token
279
+ --scheme <scheme> Require a specific x402 scheme
280
+ --max-amount <amount> Maximum human-readable payment amount
281
+ --max-rawAmount <amount> Maximum smallest-unit payment amount
282
+ --dry-run Read requirements but do not sign or pay
283
+ --private-key <hex> Explicit payer private key
284
+ --rpc-url <url> Explicit network RPC URL
285
+ --json Print JSON envelope
286
+ `,
287
+ serve: `Usage:
288
+ x402-cli serve --pay-to <address> [options]
289
+
290
+ Options:
291
+ --pay-to <address> Recipient wallet address
292
+ --amount <amount> Human-readable token amount
293
+ --rawAmount <amount> Smallest-unit amount
294
+ --network <caip2> Payment network (default: tron:nile)
295
+ --token <symbol> Token symbol (default: USDT)
296
+ --asset <address> Explicit token address
297
+ --decimals <count> Token decimals for unregistered --asset
298
+ --host <host> Bind host (default: 127.0.0.1)
299
+ --port <port> Bind port (default: 4020)
300
+ --resource-url <url> URL advertised in payment requirements
301
+ --facilitator-url <url> Facilitator base URL
302
+ --daemon Run in background and print the child pid
303
+ --json Print JSON envelope
304
+ `,
305
+ roundtrip: `Usage:
306
+ x402-cli roundtrip --pay-to <address> [serve/pay options]
307
+ `,
308
+ gateway: `Usage:
309
+ x402-cli gateway <search|start|check|scaffold|catalog> [options]
310
+
311
+ Commands:
312
+ search <query> Search a gateway/catalog artifact
313
+ start Start a local x402 gateway process
314
+ check <providers> Validate provider.yml files
315
+ scaffold <name> Write a starter provider.yml
316
+ catalog <command> Build/check/search gateway catalog assets
317
+ `,
318
+ catalog: `Usage:
319
+ x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build> [options]
320
+
321
+ Commands:
322
+ update Cache hosted/local catalog assets under ~/.cache
323
+ search <query> Search providers
324
+ show <provider> Show provider detail JSON
325
+ endpoints <provider> List provider endpoints
326
+ pay-json <provider> Print provider pay JSON
327
+ export-gateway <url> Export catalog.json and pay.md from a gateway
328
+ build <providers> Build catalog from provider.yml files
329
+
330
+ Options:
331
+ --catalog <source> catalog.json path or URL
332
+ --provider <fqn> Provider FQN for export-gateway
333
+ --output-dir <dir> Output directory for generated files
334
+ -n, --limit <count> Search result limit
335
+ --include-blocked Include blocked providers in search
336
+ --json Print JSON envelope where supported
337
+ `,
338
+ };
339
+ return sections[topic] ?? sections.root;
340
+ }
51
341
  function readYaml(file) {
52
342
  return YAML.parse(fs.readFileSync(file, "utf8"));
53
343
  }
@@ -145,6 +435,67 @@ function providerCatalog(provider) {
145
435
  })),
146
436
  };
147
437
  }
438
+ const FIELD_WEIGHTS = {
439
+ fqn: 12,
440
+ title: 10,
441
+ tags: 8,
442
+ chain_kinds: 8,
443
+ chains: 8,
444
+ category: 6,
445
+ category_meta: 6,
446
+ endpoints: 6,
447
+ i18n: 5,
448
+ description: 4,
449
+ use_case: 4,
450
+ service_url: 2,
451
+ };
452
+ function stringList(value) {
453
+ return Array.isArray(value) ? value.filter(item => item != null).map(String) : [];
454
+ }
455
+ function dictValues(value) {
456
+ if (!value || typeof value !== "object")
457
+ return [];
458
+ const out = [];
459
+ for (const child of Object.values(value)) {
460
+ if (child && typeof child === "object" && !Array.isArray(child))
461
+ out.push(...dictValues(child));
462
+ else if (Array.isArray(child))
463
+ out.push(...child.filter(item => item != null).map(String));
464
+ else if (child != null)
465
+ out.push(String(child));
466
+ }
467
+ return out;
468
+ }
469
+ function chainMetaValues(chainsMeta) {
470
+ return chainsMeta.flatMap(dictValues);
471
+ }
472
+ function endpointFields(endpoints) {
473
+ const values = [];
474
+ for (const endpoint of endpoints) {
475
+ values.push(String(endpoint.method ?? ""), String(endpoint.path ?? ""), String(endpoint.probe_status ?? ""));
476
+ const paid = endpoint.paid;
477
+ if (paid && typeof paid === "object") {
478
+ values.push(String(paid.network ?? ""), String(paid.currency ?? ""), String(paid.amount_raw ?? ""));
479
+ }
480
+ values.push(String(endpoint.title ?? ""), String(endpoint.description ?? ""), String(endpoint.use_case ?? endpoint.useCase ?? ""));
481
+ }
482
+ return values;
483
+ }
484
+ function scoreFields(terms, fields) {
485
+ let score = 0;
486
+ const matchedFields = [];
487
+ for (const [field, values] of Object.entries(fields)) {
488
+ const haystack = values.filter(Boolean).join(" ").toLowerCase();
489
+ if (!haystack)
490
+ continue;
491
+ const count = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
492
+ if (count) {
493
+ score += (FIELD_WEIGHTS[field] ?? 1) * count;
494
+ matchedFields.push(field);
495
+ }
496
+ }
497
+ return { score, matchedFields };
498
+ }
148
499
  async function readCatalog(source) {
149
500
  const text = await readText(source);
150
501
  const parsed = JSON.parse(text);
@@ -156,6 +507,13 @@ async function readCatalog(source) {
156
507
  return parsed.items;
157
508
  return [];
158
509
  }
510
+ async function readCatalogObject(source) {
511
+ const parsed = JSON.parse(await readText(source));
512
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
513
+ throw new Error(`expected JSON object from ${source}`);
514
+ }
515
+ return parsed;
516
+ }
159
517
  async function readText(source) {
160
518
  if (!source.startsWith("http://") && !source.startsWith("https://")) {
161
519
  return fs.readFileSync(source, "utf8");
@@ -168,6 +526,55 @@ async function readText(source) {
168
526
  async function readJson(source) {
169
527
  return JSON.parse(await readText(source));
170
528
  }
529
+ function writeJson(file, value) {
530
+ fs.mkdirSync(path.dirname(file), { recursive: true });
531
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
532
+ }
533
+ async function responsePayload(response) {
534
+ const text = await response.text();
535
+ const contentType = response.headers.get("content-type") ?? "";
536
+ if (contentType.toLowerCase().includes("json")) {
537
+ try {
538
+ return JSON.parse(text);
539
+ }
540
+ catch {
541
+ return text;
542
+ }
543
+ }
544
+ return text;
545
+ }
546
+ function cacheDir() {
547
+ return path.join(os.homedir(), ".cache", "x402-cli", "catalog");
548
+ }
549
+ function cachedCatalogPath() {
550
+ return path.join(cacheDir(), "catalog.json");
551
+ }
552
+ function providerFilename(fqn) {
553
+ return `${fqn.replace(/\//g, "__")}.json`;
554
+ }
555
+ function defaultCatalogSource() {
556
+ return fs.existsSync(cachedCatalogPath())
557
+ ? cachedCatalogPath()
558
+ : "https://x402-catelog.bankofai.io/api/catalog.json";
559
+ }
560
+ function remoteBaseFromCatalogPayload(payload) {
561
+ const base = payload.base_url ?? payload.baseUrl;
562
+ return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
563
+ }
564
+ async function remoteBaseFromSource(source, payload) {
565
+ const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
566
+ if (fromPayload)
567
+ return fromPayload;
568
+ if (source.startsWith("http://") || source.startsWith("https://")) {
569
+ return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
570
+ }
571
+ try {
572
+ return remoteBaseFromCatalogPayload(await readCatalogObject(source));
573
+ }
574
+ catch {
575
+ return undefined;
576
+ }
577
+ }
171
578
  function catalogDetailSource(source, section, name) {
172
579
  if (source.startsWith("http://") || source.startsWith("https://")) {
173
580
  const base = new URL(source);
@@ -183,7 +590,10 @@ function catalogDetailSource(source, section, name) {
183
590
  }
184
591
  const stat = fs.existsSync(source) ? fs.statSync(source) : undefined;
185
592
  const root = stat?.isDirectory() ? source : path.dirname(source);
186
- return path.join(root, section, `${name}.json`);
593
+ const direct = path.join(root, section, `${name}.json`);
594
+ if (fs.existsSync(direct))
595
+ return direct;
596
+ return path.join(root, section, providerFilename(name));
187
597
  }
188
598
  async function readCatalogProvider(source, name) {
189
599
  const providers = await readCatalog(source);
@@ -213,19 +623,168 @@ async function readCatalogPayProvider(source, name) {
213
623
  throw new Error(`provider not found: ${name}`);
214
624
  }
215
625
  }
626
+ async function cacheProviderAssets(source, catalogPayload) {
627
+ const base = await remoteBaseFromSource(source, catalogPayload);
628
+ if (!base)
629
+ return { detailCount: 0, payCount: 0 };
630
+ let detailCount = 0;
631
+ let payCount = 0;
632
+ for (const provider of catalogPayload.providers ?? []) {
633
+ const fqn = provider?.fqn ?? provider?.name;
634
+ if (typeof fqn !== "string" || !fqn)
635
+ continue;
636
+ const filename = providerFilename(fqn);
637
+ try {
638
+ const detail = await readJson(new URL(`providers/${filename}`, base).toString());
639
+ writeJson(path.join(cacheDir(), "providers", filename), detail);
640
+ detailCount += 1;
641
+ }
642
+ catch {
643
+ // A partial cache is still useful.
644
+ }
645
+ try {
646
+ const pay = await readJson(new URL(`pay/${filename}`, base).toString());
647
+ writeJson(path.join(cacheDir(), "pay", filename), pay);
648
+ payCount += 1;
649
+ }
650
+ catch {
651
+ // A partial cache is still useful.
652
+ }
653
+ }
654
+ return { detailCount, payCount };
655
+ }
656
+ async function catalogUpdate(source, options) {
657
+ const payload = await readCatalogObject(source);
658
+ writeJson(cachedCatalogPath(), payload);
659
+ const cached = await cacheProviderAssets(source, payload);
660
+ const result = {
661
+ source,
662
+ path: cachedCatalogPath(),
663
+ providerCount: payload.provider_count ?? payload.providerCount ?? (payload.providers ?? []).length,
664
+ detailCount: cached.detailCount,
665
+ payCount: cached.payCount,
666
+ };
667
+ emit({ command: "catalog update", mode: outputMode(options), result });
668
+ }
669
+ function zhCopy(title, subtitle, description, useCase) {
670
+ return { title, subtitle, description, useCase };
671
+ }
672
+ function submissionCatalog(detail) {
673
+ const title = String(detail.title ?? detail.fqn);
674
+ const subtitle = String(detail.subtitle ?? detail.use_case ?? title);
675
+ const description = String(detail.description ?? subtitle);
676
+ const useCase = String(detail.use_case ?? detail.useCase ?? description);
677
+ return {
678
+ version: 1,
679
+ fqn: detail.fqn,
680
+ title,
681
+ subtitle,
682
+ description,
683
+ useCase,
684
+ i18n: detail.i18n ?? { "zh-CN": zhCopy(title, subtitle, description, useCase) },
685
+ logo: detail.logo ?? "https://x402-catelog.bankofai.io/assets/providers/default.png",
686
+ category: detail.category ?? "other",
687
+ chains: detail.chains ?? [],
688
+ isFirstParty: Boolean(detail.is_first_party ?? detail.isFirstParty),
689
+ isFeatured: Boolean(detail.is_featured ?? detail.isFeatured),
690
+ featuredTags: detail.featured_tags ?? detail.featuredTags ?? [],
691
+ serviceUrl: detail.service_url ?? detail.serviceUrl,
692
+ endpoints: (detail.endpoints ?? []).map((endpoint) => {
693
+ const endpointTitle = endpoint.title ?? endpoint.path;
694
+ const endpointSubtitle = endpoint.subtitle ?? endpoint.path;
695
+ const endpointDescription = endpoint.description ?? description;
696
+ const endpointUseCase = endpoint.use_case ?? endpoint.useCase ?? useCase;
697
+ return {
698
+ method: endpoint.method,
699
+ path: endpoint.path,
700
+ url: endpoint.url,
701
+ title: endpointTitle,
702
+ subtitle: endpointSubtitle,
703
+ description: endpointDescription,
704
+ useCase: endpointUseCase,
705
+ i18n: endpoint.i18n ?? { "zh-CN": zhCopy(endpointTitle, endpointSubtitle, endpointDescription, endpointUseCase) },
706
+ metered: Boolean(endpoint.metered),
707
+ minPriceUsd: endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0,
708
+ maxPriceUsd: endpoint.max_price_usd ?? endpoint.maxPriceUsd ?? 0,
709
+ };
710
+ }),
711
+ status: detail.status ?? {
712
+ catalog: "draft",
713
+ gateway: "unknown",
714
+ payment: "unknown",
715
+ upstream: "unknown",
716
+ },
717
+ };
718
+ }
719
+ function payMarkdownFromDetail(detail) {
720
+ const lines = [
721
+ `# ${detail.title ?? detail.fqn}`,
722
+ "",
723
+ "## Service",
724
+ "",
725
+ `- FQN: \`${detail.fqn}\``,
726
+ `- Service URL: \`${detail.service_url ?? detail.serviceUrl ?? ""}\``,
727
+ `- Category: \`${detail.category ?? ""}\``,
728
+ `- Chains: \`${(detail.chains ?? []).join(", ")}\``,
729
+ "",
730
+ "## Endpoints",
731
+ "",
732
+ ];
733
+ for (const endpoint of detail.endpoints ?? []) {
734
+ const metered = Boolean(endpoint.metered);
735
+ const price = endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0;
736
+ lines.push(`### ${endpoint.method} ${endpoint.path}`, "", endpoint.description ?? "", "", `- URL: \`${endpoint.url ?? ""}\``, `- Metered: \`${String(metered)}\``, `- Price: \`$${price}\``, "");
737
+ if (metered) {
738
+ lines.push("```bash", `x402-cli pay '${endpoint.url ?? ""}'`, "```", "");
739
+ }
740
+ else {
741
+ lines.push("No payment required.", "");
742
+ }
743
+ }
744
+ lines.push("## Notes", "", "This file is public. Do not include upstream API keys, bearer tokens, provider.yml, `.env`, passwords, or private infrastructure URLs.", "");
745
+ return lines.join("\n");
746
+ }
747
+ async function catalogExportGateway(gatewayUrl, options) {
748
+ const providerFqn = opt(options, "provider");
749
+ if (!providerFqn)
750
+ throw new Error("--provider is required");
751
+ const base = gatewayUrl.replace(/\/+$/, "");
752
+ const detail = await readJson(`${base}/__402/catalog/providers/${providerFqn}.json`);
753
+ const target = opt(options, "output-dir", path.join("providers", providerFqn));
754
+ fs.mkdirSync(target, { recursive: true });
755
+ const catalogPath = path.join(target, "catalog.json");
756
+ const payMdPath = path.join(target, "pay.md");
757
+ writeJson(catalogPath, submissionCatalog(detail));
758
+ fs.writeFileSync(payMdPath, payMarkdownFromDetail(detail));
759
+ emit({ command: "catalog export-gateway", mode: outputMode(options), result: { provider: providerFqn, catalog: catalogPath, payMd: payMdPath } });
760
+ }
216
761
  function buildRequirement(options) {
217
762
  const network = normalizeNetwork(opt(options, "network", "tron:nile"));
218
763
  const tokenSymbol = opt(options, "token", "USDT");
219
- const token = getToken(network, tokenSymbol);
220
- const amount = opt(options, "rawAmount") ?? toSmallestUnit(opt(options, "amount", "0.0001"), token.decimals);
764
+ const explicitAsset = opt(options, "asset");
765
+ const registryToken = explicitAsset
766
+ ? findTokenByAddress(network, explicitAsset)
767
+ : getToken(network, tokenSymbol);
768
+ const decimalsOption = opt(options, "decimals");
769
+ if (explicitAsset && !registryToken && decimalsOption === undefined) {
770
+ throw new Error("When --asset is set without a registry match, --decimals must be provided");
771
+ }
772
+ if (!explicitAsset && !registryToken)
773
+ throw new Error(`unknown token ${tokenSymbol} on ${network}`);
774
+ const decimals = decimalsOption !== undefined ? Number(decimalsOption) : registryToken.decimals;
775
+ if (!Number.isInteger(decimals) || decimals < 0)
776
+ throw new Error("--decimals must be a non-negative integer");
777
+ const amount = opt(options, "rawAmount") ?? toSmallestUnit(opt(options, "amount", "0.0001"), decimals);
778
+ const assetAddress = explicitAsset ?? registryToken.address;
779
+ const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
221
780
  return {
222
781
  scheme: "exact",
223
782
  network,
224
783
  amount,
225
- asset: opt(options, "asset") ?? token.address,
784
+ asset: assetAddress,
226
785
  payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
227
786
  maxTimeoutSeconds: Number(opt(options, "valid-for-seconds", "300")),
228
- extra: token.assetTransferMethod ? { assetTransferMethod: token.assetTransferMethod } : {},
787
+ extra: assetTransferMethod ? { assetTransferMethod } : {},
229
788
  };
230
789
  }
231
790
  async function facilitatorPost(baseUrl, path, body) {
@@ -250,6 +809,34 @@ function requestHeaders(options) {
250
809
  }
251
810
  return headersOut;
252
811
  }
812
+ function stripFlag(argv, flag) {
813
+ return argv.filter(item => item !== flag);
814
+ }
815
+ function serveDaemon(argv, options) {
816
+ const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
817
+ const script = fileURLToPath(import.meta.url);
818
+ const child = spawn(process.execPath, [script, ...daemonArgs], {
819
+ detached: true,
820
+ stdio: "ignore",
821
+ env: process.env,
822
+ });
823
+ child.unref();
824
+ const host = opt(options, "host", "127.0.0.1");
825
+ const port = Number(opt(options, "port", "4020"));
826
+ const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
827
+ emit({
828
+ command: "server",
829
+ mode: outputMode(options),
830
+ network: normalizeNetwork(opt(options, "network", "tron:nile")),
831
+ scheme: "exact",
832
+ result: {
833
+ pid: child.pid,
834
+ pay_url: resourceUrl,
835
+ resource_url: resourceUrl,
836
+ daemon: true,
837
+ },
838
+ });
839
+ }
253
840
  function validateAmountLimits(selected, options) {
254
841
  const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
255
842
  const maxAmount = opt(options, "max-amount");
@@ -257,8 +844,15 @@ function validateAmountLimits(selected, options) {
257
844
  throw new Error(`payment raw amount ${selected.amount} exceeds --max-rawAmount ${maxRaw}`);
258
845
  }
259
846
  if (maxAmount) {
260
- const token = getToken(selected.network, opt(options, "token", "USDT"));
261
- if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, token.decimals))) {
847
+ const token = findTokenByAddress(selected.network, selected.asset);
848
+ const decimalsOption = opt(options, "decimals");
849
+ if (!token && decimalsOption === undefined) {
850
+ throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-rawAmount or --decimals");
851
+ }
852
+ const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
853
+ if (!Number.isInteger(decimals) || decimals < 0)
854
+ throw new Error("--decimals must be a non-negative integer");
855
+ if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, decimals))) {
262
856
  throw new Error(`payment amount exceeds --max-amount ${maxAmount}`);
263
857
  }
264
858
  }
@@ -266,7 +860,7 @@ function validateAmountLimits(selected, options) {
266
860
  async function serve(options) {
267
861
  const host = opt(options, "host", "127.0.0.1");
268
862
  const port = Number(opt(options, "port", "4020"));
269
- const facilitatorUrl = opt(options, "facilitator-url", process.env.FACILITATOR_URL ?? "https://facilitator.bankofai.io");
863
+ const facilitatorUrl = opt(options, "facilitator-url", "https://facilitator.bankofai.io");
270
864
  const requirement = buildRequirement(options);
271
865
  if (!requirement.payTo)
272
866
  throw new Error("--pay-to is required");
@@ -330,7 +924,13 @@ async function serve(options) {
330
924
  }
331
925
  });
332
926
  server.listen(port, host, () => {
333
- printJson({ ok: true, command: "server", network: requirement.network, scheme: "exact", result: { pay_url: resourceUrl, token: opt(options, "token", "USDT"), rawAmount: requirement.amount, pay_to: requirement.payTo } });
927
+ emit({
928
+ command: "server",
929
+ network: requirement.network,
930
+ scheme: "exact",
931
+ mode: outputMode(options),
932
+ result: { pay_url: resourceUrl, token: opt(options, "token", "USDT"), rawAmount: requirement.amount, pay_to: requirement.payTo },
933
+ });
334
934
  });
335
935
  }
336
936
  function selectRequirement(accepts, options) {
@@ -354,6 +954,8 @@ function selectRequirement(accepts, options) {
354
954
  return selected;
355
955
  }
356
956
  async function pay(url, options) {
957
+ if (!url)
958
+ throw new Error("URL is required");
357
959
  const method = opt(options, "method", "GET");
358
960
  const baseHeaders = requestHeaders(options);
359
961
  const probe = await fetch(url, {
@@ -362,7 +964,16 @@ async function pay(url, options) {
362
964
  body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : opt(options, "body"),
363
965
  });
364
966
  if (probe.status !== 402) {
365
- printJson({ ok: true, command: "client", result: { status: probe.status, body: await probe.text() } });
967
+ emit({
968
+ command: "client",
969
+ mode: outputMode(options),
970
+ result: {
971
+ url,
972
+ status: probe.status,
973
+ message: "Not a payment-required endpoint",
974
+ response: await responsePayload(probe),
975
+ },
976
+ });
366
977
  return;
367
978
  }
368
979
  const header = probe.headers.get(headers.required);
@@ -372,11 +983,11 @@ async function pay(url, options) {
372
983
  const selected = selectRequirement(required.accepts ?? [], options);
373
984
  validateAmountLimits(selected, options);
374
985
  if (options["dry-run"]) {
375
- printJson({
376
- ok: true,
986
+ emit({
377
987
  command: "client",
378
988
  network: selected.network,
379
989
  scheme: "exact",
990
+ mode: outputMode(options),
380
991
  result: {
381
992
  url,
382
993
  resource: required.resource?.url ?? url,
@@ -400,18 +1011,24 @@ async function pay(url, options) {
400
1011
  headers: retryHeaders,
401
1012
  body: opt(options, "body"),
402
1013
  });
403
- const text = await paid.text();
1014
+ const body = await responsePayload(paid);
404
1015
  const paymentResponse = paid.headers.get(headers.response);
405
- printJson({
406
- ok: paid.ok,
1016
+ const result = {
1017
+ url,
1018
+ status: paid.status,
1019
+ paid: paid.ok,
1020
+ response: body,
1021
+ ...(paymentResponse ? { paymentResponse: decodeResponse(paymentResponse) } : {}),
1022
+ };
1023
+ if (!paid.ok) {
1024
+ throw new Error(`HTTP ${paid.status} from ${url}: ${typeof body === "string" ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`);
1025
+ }
1026
+ emit({
407
1027
  command: "client",
408
1028
  network: selected.network,
409
1029
  scheme: "exact",
410
- result: {
411
- status: paid.status,
412
- body: text,
413
- ...(paymentResponse ? { paymentResponse: decodeResponse(paymentResponse) } : {}),
414
- },
1030
+ mode: outputMode(options),
1031
+ result,
415
1032
  });
416
1033
  }
417
1034
  async function roundtrip(options) {
@@ -421,12 +1038,46 @@ async function roundtrip(options) {
421
1038
  await pay(`http://127.0.0.1:${port}/pay`, options);
422
1039
  process.exit(0);
423
1040
  }
424
- function gatewayBinary() {
425
- return process.env.X402_GATEWAY_BIN ||
426
- path.resolve(process.cwd(), "../x402-gateway/dist/cli.js");
1041
+ function executableInPath(name) {
1042
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
1043
+ if (!dir)
1044
+ continue;
1045
+ const candidate = path.join(dir, name);
1046
+ try {
1047
+ fs.accessSync(candidate, fs.constants.X_OK);
1048
+ return candidate;
1049
+ }
1050
+ catch {
1051
+ // Try the next PATH entry.
1052
+ }
1053
+ }
1054
+ return undefined;
1055
+ }
1056
+ function gatewayCommand(options) {
1057
+ const explicit = opt(options, "gateway-bin");
1058
+ const candidates = [
1059
+ explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
1060
+ { file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
1061
+ executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
1062
+ { file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
1063
+ ].filter(Boolean);
1064
+ for (const candidate of candidates) {
1065
+ try {
1066
+ fs.accessSync(candidate.file, fs.constants.R_OK);
1067
+ if (candidate.file.endsWith(".js")) {
1068
+ return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
1069
+ }
1070
+ return { command: candidate.file, argsPrefix: [], source: candidate.source };
1071
+ }
1072
+ catch {
1073
+ // Try the next candidate.
1074
+ }
1075
+ }
1076
+ throw new Error("x402-gateway runtime not found. Install/publish a x402-gateway npm binary, run from a checkout with ../x402-gateway/dist/cli.js, or pass --gateway-bin <path>.");
427
1077
  }
428
- async function gatewayStart(args) {
429
- const child = spawn(process.execPath, [gatewayBinary(), ...args], {
1078
+ async function gatewayStart(args, options) {
1079
+ const gateway = gatewayCommand(options);
1080
+ const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
430
1081
  stdio: "inherit",
431
1082
  env: process.env,
432
1083
  });
@@ -435,7 +1086,7 @@ async function gatewayStart(args) {
435
1086
  child.on("error", reject);
436
1087
  });
437
1088
  }
438
- function gatewayCheck(target) {
1089
+ function gatewayCheck(target, options) {
439
1090
  const files = providerFiles(target);
440
1091
  const providers = files.map(loadProviderFile);
441
1092
  const names = new Set();
@@ -444,7 +1095,11 @@ function gatewayCheck(target) {
444
1095
  throw new Error(`duplicate provider name: ${provider.name}`);
445
1096
  names.add(provider.name);
446
1097
  }
447
- printJson({ ok: true, providers: providers.map(p => p.name), count: providers.length });
1098
+ emit({
1099
+ command: "gateway check",
1100
+ mode: outputMode(options),
1101
+ result: { providers: providers.map(p => p.name), count: providers.length },
1102
+ });
448
1103
  }
449
1104
  function gatewayScaffold(name, options) {
450
1105
  const outputDir = opt(options, "output-dir", path.join("providers", name));
@@ -465,12 +1120,12 @@ operator:
465
1120
  network: tron-nile
466
1121
  currencies:
467
1122
  usd: ["USDT"]
468
- recipient: \${X402_PROVIDER_RECIPIENT_TRON}
1123
+ recipient: <provider-recipient-address>
469
1124
  scheme: exact
470
1125
  protocol: exact
471
1126
  asset_transfer_method: permit2
472
- facilitator_url: \${X402_FACILITATOR_URL}
473
- facilitator_api_key: \${X402_FACILITATOR_API_KEY}
1127
+ facilitator_url: https://facilitator.bankofai.io
1128
+ facilitator_api_key: <facilitator-api-key>
474
1129
  valid_for_seconds: 300
475
1130
 
476
1131
  endpoints:
@@ -480,9 +1135,13 @@ endpoints:
480
1135
  dimensions:
481
1136
  - tiers:
482
1137
  - price_usd: 0.0001
483
- `;
1138
+ `;
484
1139
  fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
485
- printJson({ ok: true, file: path.join(outputDir, "provider.yml") });
1140
+ emit({
1141
+ command: "gateway scaffold",
1142
+ mode: outputMode(options),
1143
+ result: { file: path.join(outputDir, "provider.yml") },
1144
+ });
486
1145
  }
487
1146
  function catalogBuild(target, options) {
488
1147
  const providers = providerFiles(target).map(loadProviderFile).map(providerCatalog);
@@ -491,13 +1150,17 @@ function catalogBuild(target, options) {
491
1150
  if (output) {
492
1151
  fs.mkdirSync(path.dirname(output), { recursive: true });
493
1152
  fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
494
- printJson({ ok: true, output, count: providers.length });
1153
+ emit({
1154
+ command: "catalog build",
1155
+ mode: outputMode(options),
1156
+ result: { output, count: providers.length },
1157
+ });
495
1158
  }
496
1159
  else {
497
1160
  printJson(catalog);
498
1161
  }
499
1162
  }
500
- function catalogPayAssets(target) {
1163
+ function catalogPayAssets(target, options) {
501
1164
  const rows = providerFiles(target).map(loadProviderFile).flatMap(provider => (provider.endpoints ?? []).map((endpoint) => ({
502
1165
  provider: provider.name,
503
1166
  method: endpoint.method,
@@ -508,41 +1171,172 @@ function catalogPayAssets(target) {
508
1171
  scheme: "exact",
509
1172
  assetTransferMethod: providerAssetTransferMethod(provider),
510
1173
  })));
511
- printJson({ assets: rows, count: rows.length });
1174
+ if (outputMode(options) === "json") {
1175
+ printJson({ assets: rows, count: rows.length });
1176
+ }
1177
+ else {
1178
+ emit({
1179
+ command: "gateway catalog pay-assets",
1180
+ mode: "human",
1181
+ result: { count: rows.length, assets: rows },
1182
+ });
1183
+ }
512
1184
  }
513
- async function catalogSearch(source, query, options) {
1185
+ async function readProviderDetailForSearch(source, fqn) {
1186
+ try {
1187
+ return await readJson(catalogDetailSource(source, "providers", fqn));
1188
+ }
1189
+ catch {
1190
+ return {};
1191
+ }
1192
+ }
1193
+ async function searchCatalog(source, query, options) {
514
1194
  const providers = await readCatalog(source);
515
- const q = query.toLowerCase();
516
- const hits = providers
517
- .map((provider) => {
518
- const haystack = [
519
- provider.fqn,
520
- provider.name,
521
- provider.title,
522
- provider.main_title,
523
- provider.mainTitle,
524
- provider.description,
525
- provider.category,
526
- ...(provider.chains ?? []),
527
- ...(provider.featured_tags ?? []),
528
- ...(provider.featuredTags ?? []),
529
- ...(provider.tags ?? []),
530
- ...(provider.endpoints ?? []).map((e) => `${e.method} ${e.path} ${e.description ?? ""}`),
531
- ].join(" ").toLowerCase();
532
- const score = q.split(/\s+/).filter(Boolean).reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
533
- return { provider, score };
534
- })
535
- .filter(item => item.score > 0)
536
- .sort((a, b) => b.score - a.score)
1195
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1196
+ if (!terms.length)
1197
+ return [];
1198
+ const includeBlocked = hasFlag(options, "include-blocked");
1199
+ const hits = [];
1200
+ for (const provider of providers) {
1201
+ if (provider.block && !includeBlocked)
1202
+ continue;
1203
+ const fqn = String(provider.fqn ?? provider.name ?? "");
1204
+ if (!fqn)
1205
+ continue;
1206
+ const detail = await readProviderDetailForSearch(source, fqn);
1207
+ const tags = stringList(detail.featured_tags ?? provider.featured_tags ?? detail.tags ?? provider.tags);
1208
+ const endpoints = Array.isArray(detail.endpoints) ? detail.endpoints : Array.isArray(provider.endpoints) ? provider.endpoints : [];
1209
+ const categoryMeta = detail.category_meta ?? provider.category_meta;
1210
+ const chains = stringList(detail.chains ?? provider.chains);
1211
+ const chainKinds = stringList(detail.chain_kinds ?? provider.chain_kinds);
1212
+ const chainsMetaRaw = detail.chains_meta ?? provider.chains_meta ?? [];
1213
+ const chainsMeta = Array.isArray(chainsMetaRaw) ? chainsMetaRaw.filter(item => item && typeof item === "object") : [];
1214
+ const titleZh = String(detail.title_zh ?? provider.title_zh ?? "");
1215
+ const mainTitle = String(detail.main_title ?? provider.main_title ?? detail.mainTitle ?? provider.mainTitle ?? "");
1216
+ const subTitle = String(detail.sub_title ?? provider.sub_title ?? detail.subTitle ?? provider.subTitle ?? "");
1217
+ const fields = {
1218
+ fqn: [fqn],
1219
+ title: [String(detail.title ?? provider.title ?? ""), mainTitle],
1220
+ i18n: [titleZh, subTitle, ...dictValues(detail.i18n ?? provider.i18n)],
1221
+ category: [String(detail.category ?? provider.category ?? "")],
1222
+ category_meta: dictValues(categoryMeta),
1223
+ chains: [...chains, ...chainMetaValues(chainsMeta)],
1224
+ chain_kinds: chainKinds,
1225
+ service_url: [String(detail.service_url ?? provider.service_url ?? detail.serviceUrl ?? provider.serviceUrl ?? "")],
1226
+ description: [String(detail.description ?? provider.description ?? "")],
1227
+ use_case: [String(detail.use_case ?? provider.use_case ?? detail.useCase ?? provider.useCase ?? "")],
1228
+ tags,
1229
+ endpoints: endpointFields(endpoints),
1230
+ };
1231
+ const scored = scoreFields(terms, fields);
1232
+ if (scored.score === 0)
1233
+ continue;
1234
+ hits.push({
1235
+ provider,
1236
+ detail,
1237
+ fqn,
1238
+ title: fields.title[0],
1239
+ category: fields.category[0],
1240
+ serviceUrl: fields.service_url[0],
1241
+ description: fields.description[0] || undefined,
1242
+ useCase: fields.use_case[0] || undefined,
1243
+ titleZh: titleZh || undefined,
1244
+ mainTitle: mainTitle || undefined,
1245
+ subTitle: subTitle || undefined,
1246
+ categoryMeta: categoryMeta && typeof categoryMeta === "object" ? categoryMeta : undefined,
1247
+ chains,
1248
+ chainKinds,
1249
+ chainsMeta,
1250
+ tags,
1251
+ endpoints,
1252
+ score: scored.score,
1253
+ matchedFields: scored.matchedFields,
1254
+ });
1255
+ }
1256
+ return hits
1257
+ .sort((a, b) => b.score - a.score || a.fqn.localeCompare(b.fqn))
537
1258
  .slice(0, Number(opt(options, "limit", "10")));
538
- printJson({ query, count: hits.length, results: hits.map(item => item.provider) });
539
1259
  }
540
- async function catalogShow(source, name) {
541
- printJson(await readCatalogProvider(source, name));
1260
+ function searchHitToJson(hit) {
1261
+ return {
1262
+ fqn: hit.fqn,
1263
+ title: hit.title,
1264
+ category: hit.category,
1265
+ serviceUrl: hit.serviceUrl,
1266
+ description: hit.description,
1267
+ useCase: hit.useCase,
1268
+ title_zh: hit.titleZh,
1269
+ main_title: hit.mainTitle,
1270
+ sub_title: hit.subTitle,
1271
+ category_meta: hit.categoryMeta,
1272
+ chains: hit.chains,
1273
+ chain_kinds: hit.chainKinds,
1274
+ chains_meta: hit.chainsMeta,
1275
+ tags: hit.tags,
1276
+ score: hit.score,
1277
+ matchedFields: hit.matchedFields,
1278
+ endpoints: hit.endpoints,
1279
+ };
1280
+ }
1281
+ async function catalogSearch(source, query, options) {
1282
+ const hits = await searchCatalog(source, query, options);
1283
+ const results = hits.map(searchHitToJson);
1284
+ if (outputMode(options) === "json") {
1285
+ printJson({ ok: true, command: "catalog search", query, catalog: source, count: hits.length, results });
1286
+ return;
1287
+ }
1288
+ if (!hits.length) {
1289
+ process.stdout.write("no matches\n");
1290
+ return;
1291
+ }
1292
+ for (const hit of hits) {
1293
+ const tags = hit.tags.length ? hit.tags.join(",") : "-";
1294
+ process.stdout.write(`${hit.fqn.padEnd(32)} score=${String(hit.score).padEnd(3)} category=${hit.category.padEnd(12)} tags=${tags}\n`);
1295
+ if (hit.title)
1296
+ process.stdout.write(` ${hit.title}\n`);
1297
+ if (hit.description)
1298
+ process.stdout.write(` ${hit.description.split("\n")[0]}\n`);
1299
+ if (hit.serviceUrl)
1300
+ process.stdout.write(` service: ${hit.serviceUrl}\n`);
1301
+ for (const endpoint of hit.endpoints.slice(0, 3)) {
1302
+ const method = String(endpoint.method ?? "");
1303
+ const pathText = String(endpoint.path ?? endpoint.url ?? "");
1304
+ const paid = endpoint.paid;
1305
+ let suffix = "";
1306
+ if (paid && typeof paid === "object") {
1307
+ suffix = ` ${paid.network ?? ""} ${paid.currency ?? ""} ${paid.amount_raw ?? ""}`.trimEnd();
1308
+ }
1309
+ process.stdout.write(` ${method.padEnd(6)} ${pathText}${suffix ? ` ${suffix}` : ""}\n`);
1310
+ }
1311
+ process.stdout.write("\n");
1312
+ }
1313
+ }
1314
+ async function catalogShow(source, name, options) {
1315
+ const provider = await readCatalogProvider(source, name);
1316
+ if (outputMode(options) === "json") {
1317
+ printJson({ ok: true, command: "catalog show", result: provider });
1318
+ return;
1319
+ }
1320
+ process.stdout.write(`${provider.fqn ?? provider.name} - ${provider.title ?? provider.main_title ?? provider.name}\n`);
1321
+ if (provider.description)
1322
+ process.stdout.write(`${String(provider.description).split("\n")[0]}\n`);
1323
+ if (provider.category)
1324
+ process.stdout.write(`category: ${provider.category}\n`);
1325
+ if (provider.chains)
1326
+ process.stdout.write(`chains: ${provider.chains.join(", ")}\n`);
542
1327
  }
543
- async function catalogEndpoints(source, name) {
1328
+ async function catalogEndpoints(source, name, options) {
544
1329
  const provider = await readCatalogProvider(source, name);
545
- printJson({ provider: provider.fqn ?? provider.name, endpoints: provider.endpoints ?? [] });
1330
+ const endpoints = provider.endpoints ?? [];
1331
+ if (outputMode(options) === "json") {
1332
+ printJson({ ok: true, command: "catalog endpoints", provider: provider.fqn ?? provider.name, endpoints });
1333
+ return;
1334
+ }
1335
+ for (const endpoint of endpoints) {
1336
+ process.stdout.write(`${String(endpoint.method ?? "").padEnd(6)} ${endpoint.path ?? endpoint.url ?? ""}\n`);
1337
+ if (endpoint.description)
1338
+ process.stdout.write(` ${String(endpoint.description).split("\n")[0]}\n`);
1339
+ }
546
1340
  }
547
1341
  async function catalogPayJson(source, name) {
548
1342
  const provider = await readCatalogPayProvider(source, name);
@@ -560,16 +1354,22 @@ async function catalogPayJson(source, name) {
560
1354
  }
561
1355
  async function handleGateway(args) {
562
1356
  const { command, positional, options } = parseArgs(args);
563
- if (command === "start")
564
- await gatewayStart(["--providers", opt(options, "providers", opt(options, "providers-dir", positional[0] ?? "providers")), "--host", opt(options, "host", "127.0.0.1"), "--port", opt(options, "port", "4020")]);
1357
+ if (hasFlag(options, "help") || command === "help") {
1358
+ process.stdout.write(helpText("gateway"));
1359
+ return;
1360
+ }
1361
+ if (command === "search")
1362
+ await catalogSearch(opt(options, "catalog", defaultCatalogSource()), positional.join(" "), options);
1363
+ else if (command === "start")
1364
+ await gatewayStart(["--providers", opt(options, "providers", opt(options, "providers-dir", positional[0] ?? "providers")), "--host", opt(options, "host", "127.0.0.1"), "--port", opt(options, "port", "4020")], options);
565
1365
  else if (command === "check")
566
- gatewayCheck(positional[0] ?? opt(options, "providers", "providers"));
1366
+ gatewayCheck(positional[0] ?? opt(options, "providers", "providers"), options);
567
1367
  else if (command === "scaffold")
568
1368
  gatewayScaffold(positional[0] ?? "example-provider", options);
569
1369
  else if (command === "catalog")
570
1370
  await handleGatewayCatalog(positional, options);
571
1371
  else
572
- throw new Error("Usage: x402-cli gateway <start|check|scaffold|catalog>");
1372
+ throw new Error("Usage: x402-cli gateway <search|start|check|scaffold|catalog>");
573
1373
  }
574
1374
  async function handleGatewayCatalog(positional, options) {
575
1375
  const sub = positional[0] ?? "build";
@@ -577,35 +1377,56 @@ async function handleGatewayCatalog(positional, options) {
577
1377
  if (sub === "build")
578
1378
  catalogBuild(target, options);
579
1379
  else if (sub === "check")
580
- gatewayCheck(target);
1380
+ gatewayCheck(target, options);
581
1381
  else if (sub === "pay-assets")
582
- catalogPayAssets(target);
1382
+ catalogPayAssets(target, options);
583
1383
  else if (sub === "search")
584
- await catalogSearch(opt(options, "catalog", target), positional.slice(2).join(" ") || opt(options, "query", ""), options);
1384
+ await catalogSearch(opt(options, "catalog", defaultCatalogSource()), positional.slice(2).join(" ") || opt(options, "query", ""), options);
585
1385
  else
586
1386
  throw new Error("Usage: x402-cli gateway catalog <build|check|pay-assets|search>");
587
1387
  }
588
1388
  async function handleCatalog(args) {
589
1389
  const { command, positional, options } = parseArgs(args);
590
- const source = opt(options, "catalog", process.env.X402_CATALOG || "dist/catalog.json");
591
- if (command === "search")
1390
+ if (hasFlag(options, "help") || command === "help") {
1391
+ process.stdout.write(helpText("catalog"));
1392
+ return;
1393
+ }
1394
+ const source = opt(options, "catalog", defaultCatalogSource());
1395
+ if (command === "update")
1396
+ await catalogUpdate(source, options);
1397
+ else if (command === "search")
592
1398
  await catalogSearch(source, positional.join(" "), options);
593
1399
  else if (command === "show")
594
- await catalogShow(source, positional[0]);
1400
+ await catalogShow(source, positional[0], options);
595
1401
  else if (command === "endpoints")
596
- await catalogEndpoints(source, positional[0]);
1402
+ await catalogEndpoints(source, positional[0], options);
597
1403
  else if (command === "pay-json")
598
1404
  await catalogPayJson(source, positional[0]);
1405
+ else if (command === "export-gateway")
1406
+ await catalogExportGateway(positional[0], options);
599
1407
  else if (command === "build")
600
1408
  catalogBuild(positional[0] ?? "providers", options);
601
1409
  else
602
- throw new Error("Usage: x402-cli catalog <search|show|endpoints|pay-json|build>");
1410
+ throw new Error("Usage: x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build>");
603
1411
  }
604
1412
  async function main() {
605
1413
  const argv = process.argv.slice(2);
606
1414
  const { command, positional, options } = parseArgs(argv);
607
- if (command === "serve")
608
- await serve(options);
1415
+ if (command === "--help" || command === "-h" || command === "help" || hasFlag(options, "help")) {
1416
+ const topic = command === "help" ? positional[0] : command.startsWith("-") ? undefined : command;
1417
+ process.stdout.write(helpText(topic));
1418
+ return;
1419
+ }
1420
+ if (command === "--version" || command === "-V" || hasFlag(options, "version")) {
1421
+ process.stdout.write(`${getVersion()}\n`);
1422
+ return;
1423
+ }
1424
+ if (command === "serve") {
1425
+ if (hasFlag(options, "daemon"))
1426
+ serveDaemon(argv, options);
1427
+ else
1428
+ await serve(options);
1429
+ }
609
1430
  else if (command === "pay")
610
1431
  await pay(positional[0], options);
611
1432
  else if (command === "roundtrip")
@@ -615,10 +1436,14 @@ async function main() {
615
1436
  else if (command === "catalog")
616
1437
  await handleCatalog(argv.slice(1));
617
1438
  else {
618
- process.stdout.write("Usage: x402-cli <serve|pay|roundtrip|gateway|catalog> [options]\n");
1439
+ process.stdout.write(helpText());
619
1440
  }
620
1441
  }
621
1442
  main().catch(error => {
622
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1443
+ emit({
1444
+ command: process.argv[2] ?? "x402-cli",
1445
+ mode: process.argv.includes("--json") ? "json" : "human",
1446
+ error: classify(error),
1447
+ });
623
1448
  process.exit(1);
624
1449
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bankofai/x402-cli",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.json",
13
+ "test": "npm run build && node --test tests/*.test.mjs",
13
14
  "start": "node dist/cli.js",
14
15
  "dev": "tsx src/cli.ts"
15
16
  },