@bankofai/x402-cli 1.0.0-beta.6 → 1.0.0-beta.7

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 (2) hide show
  1. package/dist/cli.js +157 -47
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -10,8 +10,19 @@ import { fileURLToPath } from "node:url";
10
10
  import YAML from "yaml";
11
11
  import { createPaymentPayload, decodeRequired, decodeResponse, decodeSignature, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
12
12
  import { assertRawAmount, findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
13
- const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "version"]);
13
+ const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "raw", "version"]);
14
14
  const require = createRequire(import.meta.url);
15
+ class CliError extends Error {
16
+ code;
17
+ hint;
18
+ exitCode;
19
+ constructor(code, message, hint, exitCode = 1) {
20
+ super(message);
21
+ this.code = code;
22
+ this.hint = hint;
23
+ this.exitCode = exitCode;
24
+ }
25
+ }
15
26
  function parseArgs(argv) {
16
27
  const [command = "help", ...rest] = argv;
17
28
  const positional = [];
@@ -88,8 +99,17 @@ function hasFlag(options, key) {
88
99
  return options[key] === true;
89
100
  }
90
101
  function outputMode(options) {
102
+ if (hasFlag(options, "json") && hasFlag(options, "human")) {
103
+ throw new CliError("INVALID_ARGUMENT", "--json and --human are mutually exclusive", "Pass either --json or --human, not both.", 2);
104
+ }
91
105
  return hasFlag(options, "json") ? "json" : "human";
92
106
  }
107
+ function requireArgument(value, name, usage) {
108
+ if (value === undefined || value === "") {
109
+ throw new CliError("MISSING_ARGUMENT", `${name} is required`, `Usage: ${usage}`, 2);
110
+ }
111
+ return value;
112
+ }
93
113
  function optAll(options, key) {
94
114
  const value = options[key];
95
115
  if (Array.isArray(value))
@@ -144,6 +164,13 @@ function emit(args) {
144
164
  }
145
165
  function classify(error) {
146
166
  const message = error instanceof Error ? error.message : String(error);
167
+ if (error instanceof CliError) {
168
+ return {
169
+ code: error.code,
170
+ message,
171
+ hint: error.hint,
172
+ };
173
+ }
147
174
  const lower = message.toLowerCase();
148
175
  if (lower.includes("missing private key") || lower.includes("could not find a wallet")) {
149
176
  return {
@@ -243,6 +270,13 @@ function classify(error) {
243
270
  hint: "Check the URL, local server, proxy, and network connectivity.",
244
271
  };
245
272
  }
273
+ if (lower.includes(" is required") || lower.includes("must be") || lower.includes("invalid --") || lower.includes("mutually exclusive")) {
274
+ return {
275
+ code: lower.includes("required") ? "MISSING_ARGUMENT" : "INVALID_ARGUMENT",
276
+ message,
277
+ hint: "Run the command with --help to see valid usage and options.",
278
+ };
279
+ }
246
280
  return {
247
281
  code: "IO_ERROR",
248
282
  message,
@@ -275,24 +309,28 @@ Global options:
275
309
  Options:
276
310
  --method <method> HTTP method (default: GET)
277
311
  --header "Name: Value" Request header, repeatable
278
- --body <body> Request body for non-GET methods
312
+ --body <body> Request body for non-GET/HEAD methods
279
313
  --network <caip2> Require a specific network
280
314
  --token <symbol> Require a specific token
281
315
  --scheme <scheme> Require a specific x402 scheme
282
316
  --max-amount <amount> Maximum human-readable payment amount
283
- --max-rawAmount <amount> Maximum smallest-unit payment amount
317
+ --max-raw-amount <amount> Maximum smallest-unit payment amount
284
318
  --dry-run Read requirements but do not sign or pay
285
- --private-key <hex> Explicit payer private key
319
+ --private-key <hex> Explicit payer private key (or PRIVATE_KEY/TRON_PRIVATE_KEY/EVM_PRIVATE_KEY)
286
320
  --rpc-url <url> Explicit network RPC URL
287
321
  --json Print JSON envelope
322
+
323
+ Examples:
324
+ x402-cli pay https://api.example.com/paid --dry-run --json
325
+ x402-cli pay https://api.example.com/paid --max-amount 0.01
288
326
  `,
289
327
  serve: `Usage:
290
328
  x402-cli serve --pay-to <address> [options]
291
329
 
292
330
  Options:
293
331
  --pay-to <address> Recipient wallet address
294
- --amount <amount> Human-readable token amount
295
- --rawAmount <amount> Smallest-unit amount
332
+ --amount <amount> Human-readable token amount (default: 0.0001)
333
+ --raw-amount <amount> Smallest-unit amount
296
334
  --network <caip2> Payment network (default: tron:nile)
297
335
  --token <symbol> Token symbol (default: USDT)
298
336
  --asset <address> Explicit token address
@@ -303,6 +341,10 @@ Options:
303
341
  --facilitator-url <url> Facilitator base URL
304
342
  --daemon Run in background and print the child pid
305
343
  --json Print JSON envelope
344
+
345
+ Examples:
346
+ x402-cli serve --pay-to T... --network tron:nile --token USDT
347
+ x402-cli serve --pay-to 0x... --network eip155:97 --token USDT --amount 0.0001
306
348
  `,
307
349
  roundtrip: `Usage:
308
350
  x402-cli roundtrip --pay-to <address> [serve/pay options]
@@ -316,6 +358,15 @@ Commands:
316
358
  check <providers> Validate provider.yml files
317
359
  scaffold <name> Write a starter provider.yml
318
360
  catalog <command> Build/check/search gateway catalog assets
361
+ `,
362
+ "gateway-catalog": `Usage:
363
+ x402-cli gateway catalog <build|check|pay-assets|search> [options]
364
+
365
+ Commands:
366
+ build <providers> Build a local catalog from provider.yml files
367
+ check <providers> Validate local provider.yml files
368
+ pay-assets <providers> List payable endpoint assets
369
+ search <query> Search a catalog artifact
319
370
  `,
320
371
  catalog: `Usage:
321
372
  x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build> [options]
@@ -335,7 +386,47 @@ Options:
335
386
  --output-dir <dir> Output directory for generated files
336
387
  -n, --limit <count> Search result limit
337
388
  --include-blocked Include blocked providers in search
338
- --json Print JSON envelope where supported
389
+ --json Print JSON envelope
390
+ `,
391
+ "catalog-search": `Usage:
392
+ x402-cli catalog search <query> [--catalog <source>] [options]
393
+
394
+ Options:
395
+ --catalog <source> catalog.json path or URL
396
+ -n, --limit <count> Search result limit
397
+ --include-blocked Include blocked providers in search
398
+ --json Print JSON envelope
399
+ `,
400
+ "catalog-show": `Usage:
401
+ x402-cli catalog show <provider> [--catalog <source>] [options]
402
+
403
+ Options:
404
+ --catalog <source> catalog.json path or URL
405
+ --json Print JSON envelope
406
+ `,
407
+ "catalog-pay-json": `Usage:
408
+ x402-cli catalog pay-json <provider> [--catalog <source>] [options]
409
+
410
+ Options:
411
+ --catalog <source> catalog.json path or URL
412
+ --raw Print raw pay payload instead of JSON envelope
413
+ --json Print JSON envelope
414
+ `,
415
+ "catalog-endpoints": `Usage:
416
+ x402-cli catalog endpoints <provider> [--catalog <source>] [options]
417
+
418
+ Options:
419
+ --catalog <source> catalog.json path or URL
420
+ --json Print JSON envelope
421
+ `,
422
+ "catalog-export-gateway": `Usage:
423
+ x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]
424
+
425
+ Options:
426
+ --provider <fqn> Provider FQN to export
427
+ --output-dir <dir> Output directory for generated files
428
+ --force Overwrite existing files
429
+ --json Print JSON envelope
339
430
  `,
340
431
  };
341
432
  return sections[topic] ?? sections.root;
@@ -616,10 +707,10 @@ function defaultCatalogSource() {
616
707
  function positiveIntegerOption(options, key, fallback) {
617
708
  const value = opt(options, key, String(fallback));
618
709
  if (!/^\d+$/.test(value))
619
- throw new Error(`--${key} must be a positive integer`);
710
+ throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
620
711
  const parsed = Number(value);
621
712
  if (!Number.isSafeInteger(parsed) || parsed <= 0)
622
- throw new Error(`--${key} must be a positive integer`);
713
+ throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
623
714
  return parsed;
624
715
  }
625
716
  function remoteBaseFromCatalogPayload(payload) {
@@ -808,9 +899,8 @@ function payMarkdownFromDetail(detail) {
808
899
  return lines.join("\n");
809
900
  }
810
901
  async function catalogExportGateway(gatewayUrl, options) {
811
- const providerFqn = opt(options, "provider");
812
- if (!providerFqn)
813
- throw new Error("--provider is required");
902
+ requireArgument(gatewayUrl, "gateway-url", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
903
+ const providerFqn = requireArgument(opt(options, "provider"), "--provider", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
814
904
  sanitizeProviderName(providerFqn);
815
905
  const base = gatewayUrl.replace(/\/+$/, "");
816
906
  const detail = await readJson(`${base}/__402/catalog/providers/${providerFilename(providerFqn)}`);
@@ -846,8 +936,8 @@ function buildRequirement(options) {
846
936
  const rawAmount = opt(options, "rawAmount") ?? opt(options, "raw-amount");
847
937
  const humanAmount = opt(options, "amount");
848
938
  if (rawAmount && humanAmount)
849
- throw new Error("--amount and --rawAmount are mutually exclusive");
850
- const amount = rawAmount ? assertRawAmount(rawAmount, "--rawAmount") : toSmallestUnit(humanAmount ?? "0.0001", decimals);
939
+ throw new CliError("INVALID_ARGUMENT", "--amount and --raw-amount are mutually exclusive", "Pass either --amount or --raw-amount, not both.", 2);
940
+ const amount = rawAmount ? assertRawAmount(rawAmount, "--raw-amount") : toSmallestUnit(humanAmount ?? "0.0001", decimals);
851
941
  const assetAddress = explicitAsset ?? registryToken.address;
852
942
  const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
853
943
  return {
@@ -916,14 +1006,14 @@ function serveDaemon(argv, options) {
916
1006
  function validateAmountLimits(selected, options) {
917
1007
  const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
918
1008
  const maxAmount = opt(options, "max-amount");
919
- if (maxRaw && BigInt(selected.amount) > BigInt(assertRawAmount(maxRaw, "--max-rawAmount"))) {
920
- throw new Error(`payment raw amount ${selected.amount} exceeds --max-rawAmount ${maxRaw}`);
1009
+ if (maxRaw && BigInt(selected.amount) > BigInt(assertRawAmount(maxRaw, "--max-raw-amount"))) {
1010
+ throw new Error(`payment raw amount ${selected.amount} exceeds --max-raw-amount ${maxRaw}`);
921
1011
  }
922
1012
  if (maxAmount) {
923
1013
  const token = findTokenByAddress(selected.network, selected.asset);
924
1014
  const decimalsOption = opt(options, "decimals");
925
1015
  if (!token && decimalsOption === undefined) {
926
- throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-rawAmount or --decimals");
1016
+ throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-raw-amount or --decimals");
927
1017
  }
928
1018
  const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
929
1019
  if (!Number.isInteger(decimals) || decimals < 0)
@@ -1047,8 +1137,7 @@ function selectRequirement(accepts, options) {
1047
1137
  return selected;
1048
1138
  }
1049
1139
  async function pay(url, options) {
1050
- if (!url)
1051
- throw new Error("URL is required");
1140
+ requireArgument(url, "URL", "x402-cli pay <url> [options]");
1052
1141
  const method = opt(options, "method", "GET");
1053
1142
  const baseHeaders = requestHeaders(options);
1054
1143
  const probe = await fetch(url, {
@@ -1259,6 +1348,9 @@ function catalogBuild(target, options) {
1259
1348
  result: { output, count: providers.length },
1260
1349
  });
1261
1350
  }
1351
+ else if (outputMode(options) === "json") {
1352
+ emit({ command: "catalog build", mode: "json", result: catalog });
1353
+ }
1262
1354
  else {
1263
1355
  printJson(catalog);
1264
1356
  }
@@ -1274,16 +1366,11 @@ function catalogPayAssets(target, options) {
1274
1366
  scheme: "exact",
1275
1367
  assetTransferMethod: providerAssetTransferMethod(provider),
1276
1368
  })));
1277
- if (outputMode(options) === "json") {
1278
- printJson({ assets: rows, count: rows.length });
1279
- }
1280
- else {
1281
- emit({
1282
- command: "gateway catalog pay-assets",
1283
- mode: "human",
1284
- result: { count: rows.length, assets: rows },
1285
- });
1286
- }
1369
+ emit({
1370
+ command: "gateway catalog pay-assets",
1371
+ mode: outputMode(options),
1372
+ result: { count: rows.length, assets: rows },
1373
+ });
1287
1374
  }
1288
1375
  async function readProviderDetailForSearch(source, fqn) {
1289
1376
  try {
@@ -1382,10 +1469,11 @@ function searchHitToJson(hit) {
1382
1469
  };
1383
1470
  }
1384
1471
  async function catalogSearch(source, query, options) {
1472
+ positiveIntegerOption(options, "limit", 10);
1385
1473
  const hits = await searchCatalog(source, query, options);
1386
1474
  const results = hits.map(searchHitToJson);
1387
1475
  if (outputMode(options) === "json") {
1388
- printJson({ ok: true, command: "catalog search", query, catalog: source, count: hits.length, results });
1476
+ emit({ command: "catalog search", mode: "json", result: { query, catalog: source, count: hits.length, results } });
1389
1477
  return;
1390
1478
  }
1391
1479
  if (!hits.length) {
@@ -1415,9 +1503,10 @@ async function catalogSearch(source, query, options) {
1415
1503
  }
1416
1504
  }
1417
1505
  async function catalogShow(source, name, options) {
1506
+ requireArgument(name, "provider", "x402-cli catalog show <provider> [--catalog <source>]");
1418
1507
  const provider = await readCatalogProvider(source, name);
1419
1508
  if (outputMode(options) === "json") {
1420
- printJson({ ok: true, command: "catalog show", result: provider });
1509
+ emit({ command: "catalog show", mode: "json", result: provider });
1421
1510
  return;
1422
1511
  }
1423
1512
  process.stdout.write(`${provider.fqn ?? provider.name} - ${provider.title ?? provider.main_title ?? provider.name}\n`);
@@ -1429,10 +1518,11 @@ async function catalogShow(source, name, options) {
1429
1518
  process.stdout.write(`chains: ${provider.chains.join(", ")}\n`);
1430
1519
  }
1431
1520
  async function catalogEndpoints(source, name, options) {
1521
+ requireArgument(name, "provider", "x402-cli catalog endpoints <provider> [--catalog <source>]");
1432
1522
  const provider = await readCatalogProvider(source, name);
1433
1523
  const endpoints = provider.endpoints ?? [];
1434
1524
  if (outputMode(options) === "json") {
1435
- printJson({ ok: true, command: "catalog endpoints", provider: provider.fqn ?? provider.name, endpoints });
1525
+ emit({ command: "catalog endpoints", mode: "json", result: { provider: provider.fqn ?? provider.name, endpoints } });
1436
1526
  return;
1437
1527
  }
1438
1528
  for (const endpoint of endpoints) {
@@ -1441,28 +1531,33 @@ async function catalogEndpoints(source, name, options) {
1441
1531
  process.stdout.write(` ${String(endpoint.description).split("\n")[0]}\n`);
1442
1532
  }
1443
1533
  }
1444
- async function catalogPayJson(source, name) {
1534
+ async function catalogPayJson(source, name, options) {
1535
+ requireArgument(name, "provider", "x402-cli catalog pay-json <provider> [--catalog <source>]");
1445
1536
  const provider = await readCatalogPayProvider(source, name);
1446
1537
  const endpoint = (provider.endpoints ?? []).find((item) => item.paid || item.x402_routes?.length || item.x402Routes?.length) ?? provider.endpoints?.[0];
1447
1538
  if (!endpoint)
1448
1539
  throw new Error(`provider has no endpoints: ${name}`);
1449
- printJson({
1540
+ const result = {
1450
1541
  provider: provider.fqn ?? provider.name,
1451
1542
  url: endpoint.url ?? endpoint.path,
1452
1543
  method: endpoint.method,
1453
1544
  paid: endpoint.paid,
1454
1545
  x402_routes: endpoint.x402_routes ?? endpoint.x402Routes ?? [],
1455
1546
  endpoint,
1456
- });
1547
+ };
1548
+ if (hasFlag(options, "raw"))
1549
+ printJson(result);
1550
+ else
1551
+ emit({ command: "catalog pay-json", mode: outputMode(options), result });
1457
1552
  }
1458
1553
  async function handleGateway(args) {
1459
1554
  const { command, positional, options } = parseArgs(args);
1460
1555
  if (hasFlag(options, "help") || command === "help") {
1461
- process.stdout.write(helpText("gateway"));
1556
+ process.stdout.write(helpText(command === "catalog" || positional[0] === "catalog" ? "gateway-catalog" : "gateway"));
1462
1557
  return;
1463
1558
  }
1464
1559
  if (command === "search")
1465
- await catalogSearch(opt(options, "catalog", defaultCatalogSource()), positional.join(" "), options);
1560
+ await catalogSearch(opt(options, "catalog", defaultCatalogSource()), requireArgument(positional.join(" "), "query", "x402-cli gateway search <query> [options]"), options);
1466
1561
  else if (command === "start")
1467
1562
  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);
1468
1563
  else if (command === "check")
@@ -1472,7 +1567,7 @@ async function handleGateway(args) {
1472
1567
  else if (command === "catalog")
1473
1568
  await handleGatewayCatalog(positional, options);
1474
1569
  else
1475
- throw new Error("Usage: x402-cli gateway <search|start|check|scaffold|catalog>");
1570
+ throw new CliError("UNKNOWN_COMMAND", `Unknown gateway command: ${command}`, "Run x402-cli gateway --help to list commands.", 2);
1476
1571
  }
1477
1572
  async function handleGatewayCatalog(positional, options) {
1478
1573
  const sub = positional[0] ?? "build";
@@ -1484,37 +1579,46 @@ async function handleGatewayCatalog(positional, options) {
1484
1579
  else if (sub === "pay-assets")
1485
1580
  catalogPayAssets(target, options);
1486
1581
  else if (sub === "search")
1487
- await catalogSearch(opt(options, "catalog", defaultCatalogSource()), positional.slice(2).join(" ") || opt(options, "query", ""), options);
1582
+ await catalogSearch(opt(options, "catalog", defaultCatalogSource()), requireArgument(positional.slice(2).join(" ") || opt(options, "query"), "query", "x402-cli gateway catalog search <query> [options]"), options);
1488
1583
  else
1489
- throw new Error("Usage: x402-cli gateway catalog <build|check|pay-assets|search>");
1584
+ throw new CliError("UNKNOWN_COMMAND", `Unknown gateway catalog command: ${sub}`, "Run x402-cli gateway catalog --help to list commands.", 2);
1490
1585
  }
1491
1586
  async function handleCatalog(args) {
1492
1587
  const { command, positional, options } = parseArgs(args);
1493
1588
  if (hasFlag(options, "help") || command === "help") {
1494
- process.stdout.write(helpText("catalog"));
1589
+ const topic = command === "help" ? positional[0] : command;
1590
+ process.stdout.write(helpText(topic ? `catalog-${topic}` : "catalog"));
1495
1591
  return;
1496
1592
  }
1497
1593
  const source = opt(options, "catalog", defaultCatalogSource());
1498
1594
  if (command === "update")
1499
1595
  await catalogUpdate(source, options);
1500
1596
  else if (command === "search")
1501
- await catalogSearch(source, positional.join(" "), options);
1597
+ await catalogSearch(source, requireArgument(positional.join(" "), "query", "x402-cli catalog search <query> [options]"), options);
1502
1598
  else if (command === "show")
1503
1599
  await catalogShow(source, positional[0], options);
1504
1600
  else if (command === "endpoints")
1505
1601
  await catalogEndpoints(source, positional[0], options);
1506
1602
  else if (command === "pay-json")
1507
- await catalogPayJson(source, positional[0]);
1603
+ await catalogPayJson(source, positional[0], options);
1508
1604
  else if (command === "export-gateway")
1509
1605
  await catalogExportGateway(positional[0], options);
1510
1606
  else if (command === "build")
1511
1607
  catalogBuild(positional[0] ?? "providers", options);
1512
1608
  else
1513
- throw new Error("Usage: x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build>");
1609
+ throw new CliError("UNKNOWN_COMMAND", `Unknown catalog command: ${command}`, "Run x402-cli catalog --help to list commands.", 2);
1514
1610
  }
1515
1611
  async function main() {
1516
1612
  const argv = process.argv.slice(2);
1517
1613
  const { command, positional, options } = parseArgs(argv);
1614
+ if (hasFlag(options, "help") && command === "gateway") {
1615
+ await handleGateway(argv.slice(1));
1616
+ return;
1617
+ }
1618
+ if (hasFlag(options, "help") && command === "catalog") {
1619
+ await handleCatalog(argv.slice(1));
1620
+ return;
1621
+ }
1518
1622
  if (command === "--help" || command === "-h" || command === "help" || hasFlag(options, "help")) {
1519
1623
  const topic = command === "help" ? positional[0] : command.startsWith("-") ? undefined : command;
1520
1624
  process.stdout.write(helpText(topic));
@@ -1539,14 +1643,20 @@ async function main() {
1539
1643
  else if (command === "catalog")
1540
1644
  await handleCatalog(argv.slice(1));
1541
1645
  else {
1542
- process.stdout.write(helpText());
1646
+ throw new CliError("UNKNOWN_COMMAND", `Unknown command: ${command}`, "Run x402-cli --help to list commands.", 2);
1543
1647
  }
1544
1648
  }
1649
+ function errorCommandName(argv) {
1650
+ const [first, second] = argv;
1651
+ if ((first === "catalog" || first === "gateway") && second && !second.startsWith("-"))
1652
+ return `${first} ${second}`;
1653
+ return first ?? "x402-cli";
1654
+ }
1545
1655
  main().catch(error => {
1546
1656
  emit({
1547
- command: process.argv[2] ?? "x402-cli",
1657
+ command: errorCommandName(process.argv.slice(2)),
1548
1658
  mode: process.argv.includes("--json") ? "json" : "human",
1549
1659
  error: classify(error),
1550
1660
  });
1551
- process.exit(1);
1661
+ process.exit(error instanceof CliError ? error.exitCode : 1);
1552
1662
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bankofai/x402-cli",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0-beta.7",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"