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

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 +923 -84
  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,69 @@ 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
+ async function withSdkStdoutRedirect(enabled, fn) {
547
+ if (!enabled)
548
+ return fn();
549
+ const originalLog = console.log;
550
+ console.log = (...args) => {
551
+ process.stderr.write(`${args.map(arg => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ")}\n`);
552
+ };
553
+ try {
554
+ return await fn();
555
+ }
556
+ finally {
557
+ console.log = originalLog;
558
+ }
559
+ }
560
+ function cacheDir() {
561
+ return path.join(os.homedir(), ".cache", "x402-cli", "catalog");
562
+ }
563
+ function cachedCatalogPath() {
564
+ return path.join(cacheDir(), "catalog.json");
565
+ }
566
+ function providerFilename(fqn) {
567
+ return `${fqn.replace(/\//g, "__")}.json`;
568
+ }
569
+ function defaultCatalogSource() {
570
+ return fs.existsSync(cachedCatalogPath())
571
+ ? cachedCatalogPath()
572
+ : "https://x402-catelog.bankofai.io/api/catalog.json";
573
+ }
574
+ function remoteBaseFromCatalogPayload(payload) {
575
+ const base = payload.base_url ?? payload.baseUrl;
576
+ return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
577
+ }
578
+ async function remoteBaseFromSource(source, payload) {
579
+ const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
580
+ if (fromPayload)
581
+ return fromPayload;
582
+ if (source.startsWith("http://") || source.startsWith("https://")) {
583
+ return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
584
+ }
585
+ try {
586
+ return remoteBaseFromCatalogPayload(await readCatalogObject(source));
587
+ }
588
+ catch {
589
+ return undefined;
590
+ }
591
+ }
171
592
  function catalogDetailSource(source, section, name) {
172
593
  if (source.startsWith("http://") || source.startsWith("https://")) {
173
594
  const base = new URL(source);
@@ -183,7 +604,10 @@ function catalogDetailSource(source, section, name) {
183
604
  }
184
605
  const stat = fs.existsSync(source) ? fs.statSync(source) : undefined;
185
606
  const root = stat?.isDirectory() ? source : path.dirname(source);
186
- return path.join(root, section, `${name}.json`);
607
+ const direct = path.join(root, section, `${name}.json`);
608
+ if (fs.existsSync(direct))
609
+ return direct;
610
+ return path.join(root, section, providerFilename(name));
187
611
  }
188
612
  async function readCatalogProvider(source, name) {
189
613
  const providers = await readCatalog(source);
@@ -213,19 +637,168 @@ async function readCatalogPayProvider(source, name) {
213
637
  throw new Error(`provider not found: ${name}`);
214
638
  }
215
639
  }
640
+ async function cacheProviderAssets(source, catalogPayload) {
641
+ const base = await remoteBaseFromSource(source, catalogPayload);
642
+ if (!base)
643
+ return { detailCount: 0, payCount: 0 };
644
+ let detailCount = 0;
645
+ let payCount = 0;
646
+ for (const provider of catalogPayload.providers ?? []) {
647
+ const fqn = provider?.fqn ?? provider?.name;
648
+ if (typeof fqn !== "string" || !fqn)
649
+ continue;
650
+ const filename = providerFilename(fqn);
651
+ try {
652
+ const detail = await readJson(new URL(`providers/${filename}`, base).toString());
653
+ writeJson(path.join(cacheDir(), "providers", filename), detail);
654
+ detailCount += 1;
655
+ }
656
+ catch {
657
+ // A partial cache is still useful.
658
+ }
659
+ try {
660
+ const pay = await readJson(new URL(`pay/${filename}`, base).toString());
661
+ writeJson(path.join(cacheDir(), "pay", filename), pay);
662
+ payCount += 1;
663
+ }
664
+ catch {
665
+ // A partial cache is still useful.
666
+ }
667
+ }
668
+ return { detailCount, payCount };
669
+ }
670
+ async function catalogUpdate(source, options) {
671
+ const payload = await readCatalogObject(source);
672
+ writeJson(cachedCatalogPath(), payload);
673
+ const cached = await cacheProviderAssets(source, payload);
674
+ const result = {
675
+ source,
676
+ path: cachedCatalogPath(),
677
+ providerCount: payload.provider_count ?? payload.providerCount ?? (payload.providers ?? []).length,
678
+ detailCount: cached.detailCount,
679
+ payCount: cached.payCount,
680
+ };
681
+ emit({ command: "catalog update", mode: outputMode(options), result });
682
+ }
683
+ function zhCopy(title, subtitle, description, useCase) {
684
+ return { title, subtitle, description, useCase };
685
+ }
686
+ function submissionCatalog(detail) {
687
+ const title = String(detail.title ?? detail.fqn);
688
+ const subtitle = String(detail.subtitle ?? detail.use_case ?? title);
689
+ const description = String(detail.description ?? subtitle);
690
+ const useCase = String(detail.use_case ?? detail.useCase ?? description);
691
+ return {
692
+ version: 1,
693
+ fqn: detail.fqn,
694
+ title,
695
+ subtitle,
696
+ description,
697
+ useCase,
698
+ i18n: detail.i18n ?? { "zh-CN": zhCopy(title, subtitle, description, useCase) },
699
+ logo: detail.logo ?? "https://x402-catelog.bankofai.io/assets/providers/default.png",
700
+ category: detail.category ?? "other",
701
+ chains: detail.chains ?? [],
702
+ isFirstParty: Boolean(detail.is_first_party ?? detail.isFirstParty),
703
+ isFeatured: Boolean(detail.is_featured ?? detail.isFeatured),
704
+ featuredTags: detail.featured_tags ?? detail.featuredTags ?? [],
705
+ serviceUrl: detail.service_url ?? detail.serviceUrl,
706
+ endpoints: (detail.endpoints ?? []).map((endpoint) => {
707
+ const endpointTitle = endpoint.title ?? endpoint.path;
708
+ const endpointSubtitle = endpoint.subtitle ?? endpoint.path;
709
+ const endpointDescription = endpoint.description ?? description;
710
+ const endpointUseCase = endpoint.use_case ?? endpoint.useCase ?? useCase;
711
+ return {
712
+ method: endpoint.method,
713
+ path: endpoint.path,
714
+ url: endpoint.url,
715
+ title: endpointTitle,
716
+ subtitle: endpointSubtitle,
717
+ description: endpointDescription,
718
+ useCase: endpointUseCase,
719
+ i18n: endpoint.i18n ?? { "zh-CN": zhCopy(endpointTitle, endpointSubtitle, endpointDescription, endpointUseCase) },
720
+ metered: Boolean(endpoint.metered),
721
+ minPriceUsd: endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0,
722
+ maxPriceUsd: endpoint.max_price_usd ?? endpoint.maxPriceUsd ?? 0,
723
+ };
724
+ }),
725
+ status: detail.status ?? {
726
+ catalog: "draft",
727
+ gateway: "unknown",
728
+ payment: "unknown",
729
+ upstream: "unknown",
730
+ },
731
+ };
732
+ }
733
+ function payMarkdownFromDetail(detail) {
734
+ const lines = [
735
+ `# ${detail.title ?? detail.fqn}`,
736
+ "",
737
+ "## Service",
738
+ "",
739
+ `- FQN: \`${detail.fqn}\``,
740
+ `- Service URL: \`${detail.service_url ?? detail.serviceUrl ?? ""}\``,
741
+ `- Category: \`${detail.category ?? ""}\``,
742
+ `- Chains: \`${(detail.chains ?? []).join(", ")}\``,
743
+ "",
744
+ "## Endpoints",
745
+ "",
746
+ ];
747
+ for (const endpoint of detail.endpoints ?? []) {
748
+ const metered = Boolean(endpoint.metered);
749
+ const price = endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0;
750
+ lines.push(`### ${endpoint.method} ${endpoint.path}`, "", endpoint.description ?? "", "", `- URL: \`${endpoint.url ?? ""}\``, `- Metered: \`${String(metered)}\``, `- Price: \`$${price}\``, "");
751
+ if (metered) {
752
+ lines.push("```bash", `x402-cli pay '${endpoint.url ?? ""}'`, "```", "");
753
+ }
754
+ else {
755
+ lines.push("No payment required.", "");
756
+ }
757
+ }
758
+ lines.push("## Notes", "", "This file is public. Do not include upstream API keys, bearer tokens, provider.yml, `.env`, passwords, or private infrastructure URLs.", "");
759
+ return lines.join("\n");
760
+ }
761
+ async function catalogExportGateway(gatewayUrl, options) {
762
+ const providerFqn = opt(options, "provider");
763
+ if (!providerFqn)
764
+ throw new Error("--provider is required");
765
+ const base = gatewayUrl.replace(/\/+$/, "");
766
+ const detail = await readJson(`${base}/__402/catalog/providers/${providerFqn}.json`);
767
+ const target = opt(options, "output-dir", path.join("providers", providerFqn));
768
+ fs.mkdirSync(target, { recursive: true });
769
+ const catalogPath = path.join(target, "catalog.json");
770
+ const payMdPath = path.join(target, "pay.md");
771
+ writeJson(catalogPath, submissionCatalog(detail));
772
+ fs.writeFileSync(payMdPath, payMarkdownFromDetail(detail));
773
+ emit({ command: "catalog export-gateway", mode: outputMode(options), result: { provider: providerFqn, catalog: catalogPath, payMd: payMdPath } });
774
+ }
216
775
  function buildRequirement(options) {
217
776
  const network = normalizeNetwork(opt(options, "network", "tron:nile"));
218
777
  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);
778
+ const explicitAsset = opt(options, "asset");
779
+ const registryToken = explicitAsset
780
+ ? findTokenByAddress(network, explicitAsset)
781
+ : getToken(network, tokenSymbol);
782
+ const decimalsOption = opt(options, "decimals");
783
+ if (explicitAsset && !registryToken && decimalsOption === undefined) {
784
+ throw new Error("When --asset is set without a registry match, --decimals must be provided");
785
+ }
786
+ if (!explicitAsset && !registryToken)
787
+ throw new Error(`unknown token ${tokenSymbol} on ${network}`);
788
+ const decimals = decimalsOption !== undefined ? Number(decimalsOption) : registryToken.decimals;
789
+ if (!Number.isInteger(decimals) || decimals < 0)
790
+ throw new Error("--decimals must be a non-negative integer");
791
+ const amount = opt(options, "rawAmount") ?? toSmallestUnit(opt(options, "amount", "0.0001"), decimals);
792
+ const assetAddress = explicitAsset ?? registryToken.address;
793
+ const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
221
794
  return {
222
795
  scheme: "exact",
223
796
  network,
224
797
  amount,
225
- asset: opt(options, "asset") ?? token.address,
798
+ asset: assetAddress,
226
799
  payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
227
800
  maxTimeoutSeconds: Number(opt(options, "valid-for-seconds", "300")),
228
- extra: token.assetTransferMethod ? { assetTransferMethod: token.assetTransferMethod } : {},
801
+ extra: assetTransferMethod ? { assetTransferMethod } : {},
229
802
  };
230
803
  }
231
804
  async function facilitatorPost(baseUrl, path, body) {
@@ -250,6 +823,34 @@ function requestHeaders(options) {
250
823
  }
251
824
  return headersOut;
252
825
  }
826
+ function stripFlag(argv, flag) {
827
+ return argv.filter(item => item !== flag);
828
+ }
829
+ function serveDaemon(argv, options) {
830
+ const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
831
+ const script = fileURLToPath(import.meta.url);
832
+ const child = spawn(process.execPath, [script, ...daemonArgs], {
833
+ detached: true,
834
+ stdio: "ignore",
835
+ env: process.env,
836
+ });
837
+ child.unref();
838
+ const host = opt(options, "host", "127.0.0.1");
839
+ const port = Number(opt(options, "port", "4020"));
840
+ const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
841
+ emit({
842
+ command: "server",
843
+ mode: outputMode(options),
844
+ network: normalizeNetwork(opt(options, "network", "tron:nile")),
845
+ scheme: "exact",
846
+ result: {
847
+ pid: child.pid,
848
+ pay_url: resourceUrl,
849
+ resource_url: resourceUrl,
850
+ daemon: true,
851
+ },
852
+ });
853
+ }
253
854
  function validateAmountLimits(selected, options) {
254
855
  const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
255
856
  const maxAmount = opt(options, "max-amount");
@@ -257,8 +858,15 @@ function validateAmountLimits(selected, options) {
257
858
  throw new Error(`payment raw amount ${selected.amount} exceeds --max-rawAmount ${maxRaw}`);
258
859
  }
259
860
  if (maxAmount) {
260
- const token = getToken(selected.network, opt(options, "token", "USDT"));
261
- if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, token.decimals))) {
861
+ const token = findTokenByAddress(selected.network, selected.asset);
862
+ const decimalsOption = opt(options, "decimals");
863
+ if (!token && decimalsOption === undefined) {
864
+ throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-rawAmount or --decimals");
865
+ }
866
+ const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
867
+ if (!Number.isInteger(decimals) || decimals < 0)
868
+ throw new Error("--decimals must be a non-negative integer");
869
+ if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, decimals))) {
262
870
  throw new Error(`payment amount exceeds --max-amount ${maxAmount}`);
263
871
  }
264
872
  }
@@ -266,7 +874,7 @@ function validateAmountLimits(selected, options) {
266
874
  async function serve(options) {
267
875
  const host = opt(options, "host", "127.0.0.1");
268
876
  const port = Number(opt(options, "port", "4020"));
269
- const facilitatorUrl = opt(options, "facilitator-url", process.env.FACILITATOR_URL ?? "https://facilitator.bankofai.io");
877
+ const facilitatorUrl = opt(options, "facilitator-url", "https://facilitator.bankofai.io");
270
878
  const requirement = buildRequirement(options);
271
879
  if (!requirement.payTo)
272
880
  throw new Error("--pay-to is required");
@@ -330,7 +938,13 @@ async function serve(options) {
330
938
  }
331
939
  });
332
940
  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 } });
941
+ emit({
942
+ command: "server",
943
+ network: requirement.network,
944
+ scheme: "exact",
945
+ mode: outputMode(options),
946
+ result: { pay_url: resourceUrl, token: opt(options, "token", "USDT"), rawAmount: requirement.amount, pay_to: requirement.payTo },
947
+ });
334
948
  });
335
949
  }
336
950
  function selectRequirement(accepts, options) {
@@ -354,6 +968,8 @@ function selectRequirement(accepts, options) {
354
968
  return selected;
355
969
  }
356
970
  async function pay(url, options) {
971
+ if (!url)
972
+ throw new Error("URL is required");
357
973
  const method = opt(options, "method", "GET");
358
974
  const baseHeaders = requestHeaders(options);
359
975
  const probe = await fetch(url, {
@@ -362,7 +978,16 @@ async function pay(url, options) {
362
978
  body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : opt(options, "body"),
363
979
  });
364
980
  if (probe.status !== 402) {
365
- printJson({ ok: true, command: "client", result: { status: probe.status, body: await probe.text() } });
981
+ emit({
982
+ command: "client",
983
+ mode: outputMode(options),
984
+ result: {
985
+ url,
986
+ status: probe.status,
987
+ message: "Not a payment-required endpoint",
988
+ response: await responsePayload(probe),
989
+ },
990
+ });
366
991
  return;
367
992
  }
368
993
  const header = probe.headers.get(headers.required);
@@ -372,11 +997,11 @@ async function pay(url, options) {
372
997
  const selected = selectRequirement(required.accepts ?? [], options);
373
998
  validateAmountLimits(selected, options);
374
999
  if (options["dry-run"]) {
375
- printJson({
376
- ok: true,
1000
+ emit({
377
1001
  command: "client",
378
1002
  network: selected.network,
379
1003
  scheme: "exact",
1004
+ mode: outputMode(options),
380
1005
  result: {
381
1006
  url,
382
1007
  resource: required.resource?.url ?? url,
@@ -386,13 +1011,13 @@ async function pay(url, options) {
386
1011
  });
387
1012
  return;
388
1013
  }
389
- const payload = await createPaymentPayload({
1014
+ const payload = await withSdkStdoutRedirect(outputMode(options) === "json", () => createPaymentPayload({
390
1015
  selected,
391
1016
  resource: required.resource?.url ?? url,
392
1017
  extensions: required.extensions,
393
1018
  rpcUrl: opt(options, "rpc-url"),
394
1019
  privateKey: opt(options, "private-key"),
395
- });
1020
+ }));
396
1021
  const retryHeaders = new Headers(baseHeaders);
397
1022
  retryHeaders.set(headers.signature, encodeSignature(payload));
398
1023
  const paid = await fetch(url, {
@@ -400,18 +1025,24 @@ async function pay(url, options) {
400
1025
  headers: retryHeaders,
401
1026
  body: opt(options, "body"),
402
1027
  });
403
- const text = await paid.text();
1028
+ const body = await responsePayload(paid);
404
1029
  const paymentResponse = paid.headers.get(headers.response);
405
- printJson({
406
- ok: paid.ok,
1030
+ const result = {
1031
+ url,
1032
+ status: paid.status,
1033
+ paid: paid.ok,
1034
+ response: body,
1035
+ ...(paymentResponse ? { paymentResponse: decodeResponse(paymentResponse) } : {}),
1036
+ };
1037
+ if (!paid.ok) {
1038
+ throw new Error(`HTTP ${paid.status} from ${url}: ${typeof body === "string" ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`);
1039
+ }
1040
+ emit({
407
1041
  command: "client",
408
1042
  network: selected.network,
409
1043
  scheme: "exact",
410
- result: {
411
- status: paid.status,
412
- body: text,
413
- ...(paymentResponse ? { paymentResponse: decodeResponse(paymentResponse) } : {}),
414
- },
1044
+ mode: outputMode(options),
1045
+ result,
415
1046
  });
416
1047
  }
417
1048
  async function roundtrip(options) {
@@ -421,12 +1052,46 @@ async function roundtrip(options) {
421
1052
  await pay(`http://127.0.0.1:${port}/pay`, options);
422
1053
  process.exit(0);
423
1054
  }
424
- function gatewayBinary() {
425
- return process.env.X402_GATEWAY_BIN ||
426
- path.resolve(process.cwd(), "../x402-gateway/dist/cli.js");
1055
+ function executableInPath(name) {
1056
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
1057
+ if (!dir)
1058
+ continue;
1059
+ const candidate = path.join(dir, name);
1060
+ try {
1061
+ fs.accessSync(candidate, fs.constants.X_OK);
1062
+ return candidate;
1063
+ }
1064
+ catch {
1065
+ // Try the next PATH entry.
1066
+ }
1067
+ }
1068
+ return undefined;
427
1069
  }
428
- async function gatewayStart(args) {
429
- const child = spawn(process.execPath, [gatewayBinary(), ...args], {
1070
+ function gatewayCommand(options) {
1071
+ const explicit = opt(options, "gateway-bin");
1072
+ const candidates = [
1073
+ explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
1074
+ { file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
1075
+ executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
1076
+ { file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
1077
+ ].filter(Boolean);
1078
+ for (const candidate of candidates) {
1079
+ try {
1080
+ fs.accessSync(candidate.file, fs.constants.R_OK);
1081
+ if (candidate.file.endsWith(".js")) {
1082
+ return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
1083
+ }
1084
+ return { command: candidate.file, argsPrefix: [], source: candidate.source };
1085
+ }
1086
+ catch {
1087
+ // Try the next candidate.
1088
+ }
1089
+ }
1090
+ 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>.");
1091
+ }
1092
+ async function gatewayStart(args, options) {
1093
+ const gateway = gatewayCommand(options);
1094
+ const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
430
1095
  stdio: "inherit",
431
1096
  env: process.env,
432
1097
  });
@@ -435,7 +1100,7 @@ async function gatewayStart(args) {
435
1100
  child.on("error", reject);
436
1101
  });
437
1102
  }
438
- function gatewayCheck(target) {
1103
+ function gatewayCheck(target, options) {
439
1104
  const files = providerFiles(target);
440
1105
  const providers = files.map(loadProviderFile);
441
1106
  const names = new Set();
@@ -444,7 +1109,11 @@ function gatewayCheck(target) {
444
1109
  throw new Error(`duplicate provider name: ${provider.name}`);
445
1110
  names.add(provider.name);
446
1111
  }
447
- printJson({ ok: true, providers: providers.map(p => p.name), count: providers.length });
1112
+ emit({
1113
+ command: "gateway check",
1114
+ mode: outputMode(options),
1115
+ result: { providers: providers.map(p => p.name), count: providers.length },
1116
+ });
448
1117
  }
449
1118
  function gatewayScaffold(name, options) {
450
1119
  const outputDir = opt(options, "output-dir", path.join("providers", name));
@@ -465,12 +1134,12 @@ operator:
465
1134
  network: tron-nile
466
1135
  currencies:
467
1136
  usd: ["USDT"]
468
- recipient: \${X402_PROVIDER_RECIPIENT_TRON}
1137
+ recipient: <provider-recipient-address>
469
1138
  scheme: exact
470
1139
  protocol: exact
471
1140
  asset_transfer_method: permit2
472
- facilitator_url: \${X402_FACILITATOR_URL}
473
- facilitator_api_key: \${X402_FACILITATOR_API_KEY}
1141
+ facilitator_url: https://facilitator.bankofai.io
1142
+ facilitator_api_key: <facilitator-api-key>
474
1143
  valid_for_seconds: 300
475
1144
 
476
1145
  endpoints:
@@ -480,9 +1149,13 @@ endpoints:
480
1149
  dimensions:
481
1150
  - tiers:
482
1151
  - price_usd: 0.0001
483
- `;
1152
+ `;
484
1153
  fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
485
- printJson({ ok: true, file: path.join(outputDir, "provider.yml") });
1154
+ emit({
1155
+ command: "gateway scaffold",
1156
+ mode: outputMode(options),
1157
+ result: { file: path.join(outputDir, "provider.yml") },
1158
+ });
486
1159
  }
487
1160
  function catalogBuild(target, options) {
488
1161
  const providers = providerFiles(target).map(loadProviderFile).map(providerCatalog);
@@ -491,13 +1164,17 @@ function catalogBuild(target, options) {
491
1164
  if (output) {
492
1165
  fs.mkdirSync(path.dirname(output), { recursive: true });
493
1166
  fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
494
- printJson({ ok: true, output, count: providers.length });
1167
+ emit({
1168
+ command: "catalog build",
1169
+ mode: outputMode(options),
1170
+ result: { output, count: providers.length },
1171
+ });
495
1172
  }
496
1173
  else {
497
1174
  printJson(catalog);
498
1175
  }
499
1176
  }
500
- function catalogPayAssets(target) {
1177
+ function catalogPayAssets(target, options) {
501
1178
  const rows = providerFiles(target).map(loadProviderFile).flatMap(provider => (provider.endpoints ?? []).map((endpoint) => ({
502
1179
  provider: provider.name,
503
1180
  method: endpoint.method,
@@ -508,41 +1185,172 @@ function catalogPayAssets(target) {
508
1185
  scheme: "exact",
509
1186
  assetTransferMethod: providerAssetTransferMethod(provider),
510
1187
  })));
511
- printJson({ assets: rows, count: rows.length });
1188
+ if (outputMode(options) === "json") {
1189
+ printJson({ assets: rows, count: rows.length });
1190
+ }
1191
+ else {
1192
+ emit({
1193
+ command: "gateway catalog pay-assets",
1194
+ mode: "human",
1195
+ result: { count: rows.length, assets: rows },
1196
+ });
1197
+ }
512
1198
  }
513
- async function catalogSearch(source, query, options) {
1199
+ async function readProviderDetailForSearch(source, fqn) {
1200
+ try {
1201
+ return await readJson(catalogDetailSource(source, "providers", fqn));
1202
+ }
1203
+ catch {
1204
+ return {};
1205
+ }
1206
+ }
1207
+ async function searchCatalog(source, query, options) {
514
1208
  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)
1209
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1210
+ if (!terms.length)
1211
+ return [];
1212
+ const includeBlocked = hasFlag(options, "include-blocked");
1213
+ const hits = [];
1214
+ for (const provider of providers) {
1215
+ if (provider.block && !includeBlocked)
1216
+ continue;
1217
+ const fqn = String(provider.fqn ?? provider.name ?? "");
1218
+ if (!fqn)
1219
+ continue;
1220
+ const detail = await readProviderDetailForSearch(source, fqn);
1221
+ const tags = stringList(detail.featured_tags ?? provider.featured_tags ?? detail.tags ?? provider.tags);
1222
+ const endpoints = Array.isArray(detail.endpoints) ? detail.endpoints : Array.isArray(provider.endpoints) ? provider.endpoints : [];
1223
+ const categoryMeta = detail.category_meta ?? provider.category_meta;
1224
+ const chains = stringList(detail.chains ?? provider.chains);
1225
+ const chainKinds = stringList(detail.chain_kinds ?? provider.chain_kinds);
1226
+ const chainsMetaRaw = detail.chains_meta ?? provider.chains_meta ?? [];
1227
+ const chainsMeta = Array.isArray(chainsMetaRaw) ? chainsMetaRaw.filter(item => item && typeof item === "object") : [];
1228
+ const titleZh = String(detail.title_zh ?? provider.title_zh ?? "");
1229
+ const mainTitle = String(detail.main_title ?? provider.main_title ?? detail.mainTitle ?? provider.mainTitle ?? "");
1230
+ const subTitle = String(detail.sub_title ?? provider.sub_title ?? detail.subTitle ?? provider.subTitle ?? "");
1231
+ const fields = {
1232
+ fqn: [fqn],
1233
+ title: [String(detail.title ?? provider.title ?? ""), mainTitle],
1234
+ i18n: [titleZh, subTitle, ...dictValues(detail.i18n ?? provider.i18n)],
1235
+ category: [String(detail.category ?? provider.category ?? "")],
1236
+ category_meta: dictValues(categoryMeta),
1237
+ chains: [...chains, ...chainMetaValues(chainsMeta)],
1238
+ chain_kinds: chainKinds,
1239
+ service_url: [String(detail.service_url ?? provider.service_url ?? detail.serviceUrl ?? provider.serviceUrl ?? "")],
1240
+ description: [String(detail.description ?? provider.description ?? "")],
1241
+ use_case: [String(detail.use_case ?? provider.use_case ?? detail.useCase ?? provider.useCase ?? "")],
1242
+ tags,
1243
+ endpoints: endpointFields(endpoints),
1244
+ };
1245
+ const scored = scoreFields(terms, fields);
1246
+ if (scored.score === 0)
1247
+ continue;
1248
+ hits.push({
1249
+ provider,
1250
+ detail,
1251
+ fqn,
1252
+ title: fields.title[0],
1253
+ category: fields.category[0],
1254
+ serviceUrl: fields.service_url[0],
1255
+ description: fields.description[0] || undefined,
1256
+ useCase: fields.use_case[0] || undefined,
1257
+ titleZh: titleZh || undefined,
1258
+ mainTitle: mainTitle || undefined,
1259
+ subTitle: subTitle || undefined,
1260
+ categoryMeta: categoryMeta && typeof categoryMeta === "object" ? categoryMeta : undefined,
1261
+ chains,
1262
+ chainKinds,
1263
+ chainsMeta,
1264
+ tags,
1265
+ endpoints,
1266
+ score: scored.score,
1267
+ matchedFields: scored.matchedFields,
1268
+ });
1269
+ }
1270
+ return hits
1271
+ .sort((a, b) => b.score - a.score || a.fqn.localeCompare(b.fqn))
537
1272
  .slice(0, Number(opt(options, "limit", "10")));
538
- printJson({ query, count: hits.length, results: hits.map(item => item.provider) });
539
1273
  }
540
- async function catalogShow(source, name) {
541
- printJson(await readCatalogProvider(source, name));
1274
+ function searchHitToJson(hit) {
1275
+ return {
1276
+ fqn: hit.fqn,
1277
+ title: hit.title,
1278
+ category: hit.category,
1279
+ serviceUrl: hit.serviceUrl,
1280
+ description: hit.description,
1281
+ useCase: hit.useCase,
1282
+ title_zh: hit.titleZh,
1283
+ main_title: hit.mainTitle,
1284
+ sub_title: hit.subTitle,
1285
+ category_meta: hit.categoryMeta,
1286
+ chains: hit.chains,
1287
+ chain_kinds: hit.chainKinds,
1288
+ chains_meta: hit.chainsMeta,
1289
+ tags: hit.tags,
1290
+ score: hit.score,
1291
+ matchedFields: hit.matchedFields,
1292
+ endpoints: hit.endpoints,
1293
+ };
542
1294
  }
543
- async function catalogEndpoints(source, name) {
1295
+ async function catalogSearch(source, query, options) {
1296
+ const hits = await searchCatalog(source, query, options);
1297
+ const results = hits.map(searchHitToJson);
1298
+ if (outputMode(options) === "json") {
1299
+ printJson({ ok: true, command: "catalog search", query, catalog: source, count: hits.length, results });
1300
+ return;
1301
+ }
1302
+ if (!hits.length) {
1303
+ process.stdout.write("no matches\n");
1304
+ return;
1305
+ }
1306
+ for (const hit of hits) {
1307
+ const tags = hit.tags.length ? hit.tags.join(",") : "-";
1308
+ process.stdout.write(`${hit.fqn.padEnd(32)} score=${String(hit.score).padEnd(3)} category=${hit.category.padEnd(12)} tags=${tags}\n`);
1309
+ if (hit.title)
1310
+ process.stdout.write(` ${hit.title}\n`);
1311
+ if (hit.description)
1312
+ process.stdout.write(` ${hit.description.split("\n")[0]}\n`);
1313
+ if (hit.serviceUrl)
1314
+ process.stdout.write(` service: ${hit.serviceUrl}\n`);
1315
+ for (const endpoint of hit.endpoints.slice(0, 3)) {
1316
+ const method = String(endpoint.method ?? "");
1317
+ const pathText = String(endpoint.path ?? endpoint.url ?? "");
1318
+ const paid = endpoint.paid;
1319
+ let suffix = "";
1320
+ if (paid && typeof paid === "object") {
1321
+ suffix = ` ${paid.network ?? ""} ${paid.currency ?? ""} ${paid.amount_raw ?? ""}`.trimEnd();
1322
+ }
1323
+ process.stdout.write(` ${method.padEnd(6)} ${pathText}${suffix ? ` ${suffix}` : ""}\n`);
1324
+ }
1325
+ process.stdout.write("\n");
1326
+ }
1327
+ }
1328
+ async function catalogShow(source, name, options) {
544
1329
  const provider = await readCatalogProvider(source, name);
545
- printJson({ provider: provider.fqn ?? provider.name, endpoints: provider.endpoints ?? [] });
1330
+ if (outputMode(options) === "json") {
1331
+ printJson({ ok: true, command: "catalog show", result: provider });
1332
+ return;
1333
+ }
1334
+ process.stdout.write(`${provider.fqn ?? provider.name} - ${provider.title ?? provider.main_title ?? provider.name}\n`);
1335
+ if (provider.description)
1336
+ process.stdout.write(`${String(provider.description).split("\n")[0]}\n`);
1337
+ if (provider.category)
1338
+ process.stdout.write(`category: ${provider.category}\n`);
1339
+ if (provider.chains)
1340
+ process.stdout.write(`chains: ${provider.chains.join(", ")}\n`);
1341
+ }
1342
+ async function catalogEndpoints(source, name, options) {
1343
+ const provider = await readCatalogProvider(source, name);
1344
+ const endpoints = provider.endpoints ?? [];
1345
+ if (outputMode(options) === "json") {
1346
+ printJson({ ok: true, command: "catalog endpoints", provider: provider.fqn ?? provider.name, endpoints });
1347
+ return;
1348
+ }
1349
+ for (const endpoint of endpoints) {
1350
+ process.stdout.write(`${String(endpoint.method ?? "").padEnd(6)} ${endpoint.path ?? endpoint.url ?? ""}\n`);
1351
+ if (endpoint.description)
1352
+ process.stdout.write(` ${String(endpoint.description).split("\n")[0]}\n`);
1353
+ }
546
1354
  }
547
1355
  async function catalogPayJson(source, name) {
548
1356
  const provider = await readCatalogPayProvider(source, name);
@@ -560,16 +1368,22 @@ async function catalogPayJson(source, name) {
560
1368
  }
561
1369
  async function handleGateway(args) {
562
1370
  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")]);
1371
+ if (hasFlag(options, "help") || command === "help") {
1372
+ process.stdout.write(helpText("gateway"));
1373
+ return;
1374
+ }
1375
+ if (command === "search")
1376
+ await catalogSearch(opt(options, "catalog", defaultCatalogSource()), positional.join(" "), options);
1377
+ else if (command === "start")
1378
+ 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
1379
  else if (command === "check")
566
- gatewayCheck(positional[0] ?? opt(options, "providers", "providers"));
1380
+ gatewayCheck(positional[0] ?? opt(options, "providers", "providers"), options);
567
1381
  else if (command === "scaffold")
568
1382
  gatewayScaffold(positional[0] ?? "example-provider", options);
569
1383
  else if (command === "catalog")
570
1384
  await handleGatewayCatalog(positional, options);
571
1385
  else
572
- throw new Error("Usage: x402-cli gateway <start|check|scaffold|catalog>");
1386
+ throw new Error("Usage: x402-cli gateway <search|start|check|scaffold|catalog>");
573
1387
  }
574
1388
  async function handleGatewayCatalog(positional, options) {
575
1389
  const sub = positional[0] ?? "build";
@@ -577,35 +1391,56 @@ async function handleGatewayCatalog(positional, options) {
577
1391
  if (sub === "build")
578
1392
  catalogBuild(target, options);
579
1393
  else if (sub === "check")
580
- gatewayCheck(target);
1394
+ gatewayCheck(target, options);
581
1395
  else if (sub === "pay-assets")
582
- catalogPayAssets(target);
1396
+ catalogPayAssets(target, options);
583
1397
  else if (sub === "search")
584
- await catalogSearch(opt(options, "catalog", target), positional.slice(2).join(" ") || opt(options, "query", ""), options);
1398
+ await catalogSearch(opt(options, "catalog", defaultCatalogSource()), positional.slice(2).join(" ") || opt(options, "query", ""), options);
585
1399
  else
586
1400
  throw new Error("Usage: x402-cli gateway catalog <build|check|pay-assets|search>");
587
1401
  }
588
1402
  async function handleCatalog(args) {
589
1403
  const { command, positional, options } = parseArgs(args);
590
- const source = opt(options, "catalog", process.env.X402_CATALOG || "dist/catalog.json");
591
- if (command === "search")
1404
+ if (hasFlag(options, "help") || command === "help") {
1405
+ process.stdout.write(helpText("catalog"));
1406
+ return;
1407
+ }
1408
+ const source = opt(options, "catalog", defaultCatalogSource());
1409
+ if (command === "update")
1410
+ await catalogUpdate(source, options);
1411
+ else if (command === "search")
592
1412
  await catalogSearch(source, positional.join(" "), options);
593
1413
  else if (command === "show")
594
- await catalogShow(source, positional[0]);
1414
+ await catalogShow(source, positional[0], options);
595
1415
  else if (command === "endpoints")
596
- await catalogEndpoints(source, positional[0]);
1416
+ await catalogEndpoints(source, positional[0], options);
597
1417
  else if (command === "pay-json")
598
1418
  await catalogPayJson(source, positional[0]);
1419
+ else if (command === "export-gateway")
1420
+ await catalogExportGateway(positional[0], options);
599
1421
  else if (command === "build")
600
1422
  catalogBuild(positional[0] ?? "providers", options);
601
1423
  else
602
- throw new Error("Usage: x402-cli catalog <search|show|endpoints|pay-json|build>");
1424
+ throw new Error("Usage: x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build>");
603
1425
  }
604
1426
  async function main() {
605
1427
  const argv = process.argv.slice(2);
606
1428
  const { command, positional, options } = parseArgs(argv);
607
- if (command === "serve")
608
- await serve(options);
1429
+ if (command === "--help" || command === "-h" || command === "help" || hasFlag(options, "help")) {
1430
+ const topic = command === "help" ? positional[0] : command.startsWith("-") ? undefined : command;
1431
+ process.stdout.write(helpText(topic));
1432
+ return;
1433
+ }
1434
+ if (command === "--version" || command === "-V" || hasFlag(options, "version")) {
1435
+ process.stdout.write(`${getVersion()}\n`);
1436
+ return;
1437
+ }
1438
+ if (command === "serve") {
1439
+ if (hasFlag(options, "daemon"))
1440
+ serveDaemon(argv, options);
1441
+ else
1442
+ await serve(options);
1443
+ }
609
1444
  else if (command === "pay")
610
1445
  await pay(positional[0], options);
611
1446
  else if (command === "roundtrip")
@@ -615,10 +1450,14 @@ async function main() {
615
1450
  else if (command === "catalog")
616
1451
  await handleCatalog(argv.slice(1));
617
1452
  else {
618
- process.stdout.write("Usage: x402-cli <serve|pay|roundtrip|gateway|catalog> [options]\n");
1453
+ process.stdout.write(helpText());
619
1454
  }
620
1455
  }
621
1456
  main().catch(error => {
622
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1457
+ emit({
1458
+ command: process.argv[2] ?? "x402-cli",
1459
+ mode: process.argv.includes("--json") ? "json" : "human",
1460
+ error: classify(error),
1461
+ });
623
1462
  process.exit(1);
624
1463
  });
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.2",
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
  },