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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,12 +3,13 @@
3
3
  TypeScript command-line client for BankofAI x402 payments. This version uses
4
4
  the npm TypeScript SDK packages only:
5
5
 
6
- - `@bankofai/x402-core@1.0.0`
7
- - `@bankofai/x402-evm@1.0.0`
8
- - `@bankofai/x402-tron@1.0.0`
6
+ - `@bankofai/x402-core@1.0.1-beta.2`
7
+ - `@bankofai/x402-evm@1.0.1-beta.2`
8
+ - `@bankofai/x402-tron@1.0.1-beta.2`
9
9
 
10
- Stablecoin payments use `scheme=exact` with
11
- `extra.assetTransferMethod=permit2`.
10
+ Stablecoin payments support `scheme=exact` and TRON `scheme=exact_gasfree`.
11
+ The GasFree flow lets the relayer pay network energy while deducting its fee
12
+ from the payment token, so the payer does not need TRX.
12
13
 
13
14
  ## Install
14
15
 
@@ -74,6 +75,20 @@ node dist/cli.js pay http://127.0.0.1:4020/pay \
74
75
  --token USDT
75
76
  ```
76
77
 
78
+ Pay a TRON GasFree endpoint (the CLI normally selects this automatically from
79
+ the server challenge):
80
+
81
+ ```bash
82
+ TRON_PRIVATE_KEY=<hex> \
83
+ node dist/cli.js pay https://api.example.com/pay \
84
+ --network tron:nile \
85
+ --token USDT \
86
+ --scheme exact_gasfree
87
+ ```
88
+
89
+ Use `--gasfree-api-url <url>` or `X402_GASFREE_API_URL` to override the SDK's
90
+ default relayer endpoint.
91
+
77
92
  For EVM networks use `EVM_PRIVATE_KEY` or `PRIVATE_KEY`.
78
93
 
79
94
  ### Roundtrip
@@ -114,5 +129,6 @@ Pass a facilitator URL when needed:
114
129
  x402-cli serve --facilitator-url https://facilitator.bankofai.io ...
115
130
  ```
116
131
 
117
- CLI payment challenges and payload selection always emit `scheme: "exact"` for
118
- the SDK 1.0 Permit2 path.
132
+ `serve --scheme exact_gasfree` advertises a TRON GasFree requirement. The
133
+ configured facilitator must advertise and settle `exact_gasfree` for that
134
+ network and token.
package/dist/cli.js CHANGED
@@ -315,6 +315,7 @@ Options:
315
315
  --network <caip2> Require a specific network
316
316
  --token <symbol> Require a specific token
317
317
  --scheme <scheme> Require a specific x402 scheme
318
+ --gasfree-api-url <url> Override the TRON GasFree relayer API URL
318
319
  --max-amount <amount> Maximum human-readable payment amount
319
320
  --max-raw-amount <amount> Maximum smallest-unit payment amount
320
321
  --dry-run Read requirements but do not sign or pay
@@ -335,6 +336,7 @@ Options:
335
336
  --amount <amount> Human-readable token amount (default: 0.0001)
336
337
  --raw-amount <amount> Smallest-unit amount
337
338
  --network <caip2> Payment network (default: tron:nile)
339
+ --scheme <scheme> Payment scheme: exact or exact_gasfree (default: exact)
338
340
  --token <symbol> Token symbol (default: USDT)
339
341
  --asset <address> Explicit token address
340
342
  --decimals <count> Token decimals for unregistered --asset
@@ -476,7 +478,7 @@ function loadProviderFile(file) {
476
478
  const provider = expandDeep(readYaml(file));
477
479
  validateProvider(provider, file);
478
480
  provider.operator.network = normalizeNetwork(provider.operator.network);
479
- provider.operator.scheme = "exact";
481
+ provider.operator.scheme = provider.operator.scheme ?? "exact";
480
482
  return provider;
481
483
  }
482
484
  function validateProvider(provider, file = "provider.yml") {
@@ -524,6 +526,9 @@ function providerPrice(endpoint) {
524
526
  function providerAssetTransferMethod(provider) {
525
527
  return provider.operator?.asset_transfer_method ?? provider.operator?.assetTransferMethod ?? "permit2";
526
528
  }
529
+ function providerScheme(provider) {
530
+ return provider.operator?.scheme ?? "exact";
531
+ }
527
532
  function providerCatalog(provider) {
528
533
  return {
529
534
  name: provider.name,
@@ -540,7 +545,7 @@ function providerCatalog(provider) {
540
545
  upstream_path: endpoint.path,
541
546
  description: endpoint.description ?? "",
542
547
  paid: providerPrice(endpoint) > 0 ? {
543
- scheme: "exact",
548
+ scheme: providerScheme(provider),
544
549
  network: normalizeNetwork(provider.operator.network),
545
550
  currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
546
551
  price_usd: providerPrice(endpoint),
@@ -548,7 +553,7 @@ function providerCatalog(provider) {
548
553
  x402_routes: providerPrice(endpoint) > 0 ? [{
549
554
  provider: provider.name,
550
555
  network: normalizeNetwork(provider.operator.network),
551
- scheme: "exact",
556
+ scheme: providerScheme(provider),
552
557
  assetTransferMethod: providerAssetTransferMethod(provider),
553
558
  url: `/providers/${provider.name}${endpoint.path}`,
554
559
  }] : [],
@@ -964,6 +969,12 @@ async function catalogExportGateway(gatewayUrl, options) {
964
969
  }
965
970
  function buildRequirement(options) {
966
971
  const network = normalizeNetwork(opt(options, "network", "tron:nile"));
972
+ const scheme = opt(options, "scheme", "exact");
973
+ if (!["exact", "exact_gasfree"].includes(scheme))
974
+ throw new Error(`unsupported scheme ${scheme}`);
975
+ if (scheme === "exact_gasfree" && !network.startsWith("tron:")) {
976
+ throw new Error("exact_gasfree is supported only on TRON networks");
977
+ }
967
978
  const tokenSymbol = opt(options, "token", "USDT");
968
979
  const explicitAsset = opt(options, "asset");
969
980
  const registryToken = explicitAsset
@@ -986,13 +997,13 @@ function buildRequirement(options) {
986
997
  const assetAddress = explicitAsset ?? registryToken.address;
987
998
  const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
988
999
  return {
989
- scheme: "exact",
1000
+ scheme,
990
1001
  network,
991
1002
  amount,
992
1003
  asset: assetAddress,
993
1004
  payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
994
1005
  maxTimeoutSeconds: Number(opt(options, "valid-for-seconds", "300")),
995
- extra: assetTransferMethod ? { assetTransferMethod } : {},
1006
+ extra: scheme === "exact" && assetTransferMethod ? { assetTransferMethod } : {},
996
1007
  };
997
1008
  }
998
1009
  async function facilitatorPost(baseUrl, path, body, options) {
@@ -1039,7 +1050,7 @@ function serveDaemon(argv, options) {
1039
1050
  command: "server",
1040
1051
  mode: outputMode(options),
1041
1052
  network: requirement.network,
1042
- scheme: "exact",
1053
+ scheme: requirement.scheme,
1043
1054
  result: {
1044
1055
  pid: child.pid,
1045
1056
  pay_url: resourceUrl,
@@ -1092,7 +1103,7 @@ async function serve(options) {
1092
1103
  }
1093
1104
  if (pathname === "/.well-known/x402") {
1094
1105
  response.writeHead(200, { "content-type": "application/json" });
1095
- response.end(JSON.stringify({ network: requirement.network, scheme: "exact", asset: requirement.asset, rawAmount: requirement.amount, amount: requirement.amount, payTo: requirement.payTo, pay_url: resourceUrl }));
1106
+ response.end(JSON.stringify({ network: requirement.network, scheme: requirement.scheme, asset: requirement.asset, rawAmount: requirement.amount, amount: requirement.amount, payTo: requirement.payTo, pay_url: resourceUrl }));
1096
1107
  return;
1097
1108
  }
1098
1109
  if (pathname !== "/pay") {
@@ -1139,7 +1150,7 @@ async function serve(options) {
1139
1150
  "content-type": "application/json",
1140
1151
  [headers.response]: encodeResponse(settle),
1141
1152
  });
1142
- response.end(JSON.stringify({ success: true, network: requirement.network, scheme: "exact", transaction: settle.transaction ?? settle.txHash ?? null }));
1153
+ response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction ?? settle.txHash ?? null }));
1143
1154
  }
1144
1155
  catch (error) {
1145
1156
  response.writeHead(500, { "content-type": "application/json" });
@@ -1153,7 +1164,7 @@ async function serve(options) {
1153
1164
  emit({
1154
1165
  command: "server",
1155
1166
  network: requirement.network,
1156
- scheme: "exact",
1167
+ scheme: requirement.scheme,
1157
1168
  mode: outputMode(options),
1158
1169
  result: { pay_url: resourceUrl, token: opt(options, "token", "USDT"), rawAmount: requirement.amount, pay_to: requirement.payTo },
1159
1170
  });
@@ -1213,7 +1224,7 @@ async function pay(url, options) {
1213
1224
  emit({
1214
1225
  command: "client",
1215
1226
  network: selected.network,
1216
- scheme: "exact",
1227
+ scheme: selected.scheme,
1217
1228
  mode: outputMode(options),
1218
1229
  result: {
1219
1230
  url,
@@ -1230,6 +1241,7 @@ async function pay(url, options) {
1230
1241
  extensions: required.extensions,
1231
1242
  rpcUrl: opt(options, "rpc-url"),
1232
1243
  privateKey: opt(options, "private-key"),
1244
+ gasfreeApiUrl: opt(options, "gasfree-api-url"),
1233
1245
  }));
1234
1246
  const retryHeaders = new Headers(baseHeaders);
1235
1247
  retryHeaders.set(headers.signature, encodeSignature(payload));
@@ -1253,7 +1265,7 @@ async function pay(url, options) {
1253
1265
  emit({
1254
1266
  command: "client",
1255
1267
  network: selected.network,
1256
- scheme: "exact",
1268
+ scheme: selected.scheme,
1257
1269
  mode: outputMode(options),
1258
1270
  result,
1259
1271
  });
@@ -1408,7 +1420,7 @@ function catalogPayAssets(target, options) {
1408
1420
  network: normalizeNetwork(provider.operator.network),
1409
1421
  currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
1410
1422
  price_usd: providerPrice(endpoint),
1411
- scheme: "exact",
1423
+ scheme: providerScheme(provider),
1412
1424
  assetTransferMethod: providerAssetTransferMethod(provider),
1413
1425
  })));
1414
1426
  emit({
@@ -65,17 +65,21 @@ function opt(options, key, fallback) {
65
65
  function flag(options, key) {
66
66
  return options[key] === true;
67
67
  }
68
- function version() {
68
+ function rawHasFlag(argv, name, short) {
69
+ return argv.some(item => item === `--${name}` || item.startsWith(`--${name}=`) || (short ? item === short : false));
70
+ }
71
+ function readVersion() {
69
72
  try {
70
73
  const url = new URL("../package.json", import.meta.url);
71
- return String(JSON.parse(fs.readFileSync(url, "utf8")).version ?? "0.0.0");
74
+ const version = JSON.parse(fs.readFileSync(url, "utf8")).version;
75
+ return typeof version === "string" && version ? version : undefined;
72
76
  }
73
77
  catch {
74
- return "0.0.0";
78
+ return undefined;
75
79
  }
76
80
  }
77
81
  function help() {
78
- return `x402-gateway ${version()}
82
+ return `x402-gateway ${readVersion() ?? "unknown"}
79
83
 
80
84
  Usage:
81
85
  x402-gateway start --providers <dir> [options]
@@ -105,6 +109,9 @@ Examples:
105
109
  `;
106
110
  }
107
111
  function providerPath(options) {
112
+ if (opt(options, "provider") && opt(options, "providers")) {
113
+ throw new CliError("Options --provider and --providers are mutually exclusive.");
114
+ }
108
115
  const source = opt(options, "providers", opt(options, "provider", process.env.X402_GATEWAY_PROVIDERS_DIR));
109
116
  if (!source)
110
117
  throw new CliError("No provider source configured. Use --provider <file> or --providers <dir>.");
@@ -120,25 +127,56 @@ function parsePort(value) {
120
127
  }
121
128
  return port;
122
129
  }
130
+ function parseHost(value) {
131
+ const host = value ?? process.env.X402_GATEWAY_HOST ?? "127.0.0.1";
132
+ if (!host.trim())
133
+ throw new CliError("Invalid --host; expected a non-empty host.");
134
+ return host;
135
+ }
136
+ function listenErrorMessage(error, host, port) {
137
+ const err = error;
138
+ if (err?.code === "EADDRINUSE")
139
+ return `Port ${port} is already in use on ${host}. Choose another --port or stop the existing process.`;
140
+ if (err?.code === "EADDRNOTAVAIL")
141
+ return `Host ${host} is not available on this machine. Use --host 127.0.0.1 for local runs or --host 0.0.0.0 in containers.`;
142
+ return error instanceof Error ? error.message : String(error);
143
+ }
123
144
  function publicHost(host) {
124
145
  return host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
125
146
  }
126
- function printStartup(options, host, port, providers) {
147
+ function localUrls(host, port) {
148
+ const base = `http://${publicHost(host)}:${port}`;
149
+ return {
150
+ base,
151
+ health: `${base}/__402/health`,
152
+ ready: `${base}/__402/ready`,
153
+ };
154
+ }
155
+ function printStartup(options, source, host, port, providers) {
127
156
  if (flag(options, "quiet"))
128
157
  return;
158
+ const urls = localUrls(host, port);
129
159
  if (flag(options, "json")) {
130
- process.stdout.write(JSON.stringify({ ok: true, host, port, providers }, null, 2) + "\n");
160
+ process.stdout.write(JSON.stringify({ ok: true, source, host, port, count: providers.length, providers, health: urls.health, ready: urls.ready }, null, 2) + "\n");
131
161
  return;
132
162
  }
133
- const base = `http://${publicHost(host)}:${port}`;
134
- process.stdout.write(`x402-gateway listening on ${base}\n`);
163
+ process.stdout.write(`x402-gateway listening on ${urls.base}\n`);
135
164
  process.stdout.write(`providers: ${providers.length} loaded\n`);
136
- process.stdout.write(`health: ${base}/__402/health\n`);
137
- process.stdout.write(`ready: ${base}/__402/ready\n`);
165
+ process.stdout.write(`health: ${urls.health}\n`);
166
+ process.stdout.write(`ready: ${urls.ready}\n`);
167
+ }
168
+ function loadProvidersForCli(action, source) {
169
+ try {
170
+ return loadProviders(source);
171
+ }
172
+ catch (error) {
173
+ const message = error instanceof Error ? error.message : String(error);
174
+ throw new Error(`failed to load providers from ${source} for ${action}: ${message}`);
175
+ }
138
176
  }
139
177
  function check(options) {
140
178
  const source = providerPath(options);
141
- const providers = loadProviders(source);
179
+ const providers = loadProvidersForCli("check", source);
142
180
  const summary = [...providers.values()].map(entry => ({
143
181
  name: entry.config.name,
144
182
  network: entry.config.operator.network,
@@ -148,6 +186,8 @@ function check(options) {
148
186
  process.stdout.write(JSON.stringify({ ok: true, source, count: summary.length, providers: summary }, null, 2) + "\n");
149
187
  return;
150
188
  }
189
+ if (flag(options, "quiet"))
190
+ return;
151
191
  process.stdout.write(`ok: ${summary.length} provider${summary.length === 1 ? "" : "s"} loaded from ${source}\n`);
152
192
  for (const item of summary) {
153
193
  process.stdout.write(` ${item.name} (${item.network}) endpoints=${item.endpoints}\n`);
@@ -155,22 +195,29 @@ function check(options) {
155
195
  }
156
196
  async function start(options) {
157
197
  const source = providerPath(options);
158
- const host = opt(options, "host", process.env.X402_GATEWAY_HOST || "127.0.0.1");
198
+ const host = parseHost(opt(options, "host"));
159
199
  const port = parsePort(opt(options, "port"));
160
- const providers = loadProviders(source);
200
+ const providers = loadProvidersForCli("start", source);
161
201
  const server = createGatewayServer(providers);
162
202
  await new Promise((resolve, reject) => {
163
- server.once("error", reject);
203
+ server.once("error", error => reject(new Error(listenErrorMessage(error, host, port))));
164
204
  server.listen(port, host, () => {
165
205
  server.off("error", reject);
166
- printStartup(options, host, port, [...providers.keys()]);
206
+ printStartup(options, source, host, port, [...providers.keys()]);
167
207
  resolve();
168
208
  });
169
209
  });
170
210
  const shutdown = (signal) => {
171
211
  if (!flag(options, "quiet") && !flag(options, "json"))
172
212
  process.stderr.write(`received ${signal}, shutting down...\n`);
213
+ const timeout = setTimeout(() => {
214
+ if (!flag(options, "quiet") && !flag(options, "json"))
215
+ process.stderr.write("server close timed out; exiting\n");
216
+ process.exit(1);
217
+ }, 10_000);
218
+ timeout.unref();
173
219
  server.close(() => {
220
+ clearTimeout(timeout);
174
221
  if (!flag(options, "quiet") && !flag(options, "json"))
175
222
  process.stderr.write("server closed\n");
176
223
  process.exit(0);
@@ -186,7 +233,10 @@ async function main() {
186
233
  return;
187
234
  }
188
235
  if (flag(parsed.options, "version")) {
189
- process.stdout.write(`${version()}\n`);
236
+ const version = readVersion();
237
+ if (!version)
238
+ throw new Error("Unable to read package version.");
239
+ process.stdout.write(`${version}\n`);
190
240
  return;
191
241
  }
192
242
  if (parsed.command === "check")
@@ -195,22 +245,17 @@ async function main() {
195
245
  await start(parsed.options);
196
246
  }
197
247
  main().catch(error => {
198
- const parsed = (() => {
199
- try {
200
- return parseArgs(process.argv.slice(2));
201
- }
202
- catch {
203
- return { options: {} };
204
- }
205
- })();
248
+ const argv = process.argv.slice(2);
249
+ const json = rawHasFlag(argv, "json");
250
+ const debug = rawHasFlag(argv, "debug");
206
251
  const message = error instanceof Error ? error.message : String(error);
207
- if (flag(parsed.options, "json")) {
252
+ if (json) {
208
253
  process.stdout.write(JSON.stringify({ ok: false, error: { message } }, null, 2) + "\n");
209
254
  }
210
255
  else {
211
256
  process.stderr.write(`x402-gateway: ${message}\n`);
212
257
  process.stderr.write("Run `x402-gateway --help` for usage.\n");
213
- if (flag(parsed.options, "debug") && error instanceof Error && error.stack) {
258
+ if (debug && error instanceof Error && error.stack) {
214
259
  process.stderr.write(`${error.stack}\n`);
215
260
  }
216
261
  }
package/dist/x402.js CHANGED
@@ -2,6 +2,7 @@ import { decodePaymentRequiredHeader, decodePaymentResponseHeader, decodePayment
2
2
  import { x402Client } from "@bankofai/x402-core/client";
3
3
  import { ExactEvmScheme, toClientEvmSigner } from "@bankofai/x402-evm";
4
4
  import { ExactTronScheme, createClientTronSigner } from "@bankofai/x402-tron";
5
+ import { registerExactGasFreeTronScheme } from "@bankofai/x402-tron/gasfree/client";
5
6
  import { createPublicClient, http } from "viem";
6
7
  import { privateKeyToAccount } from "viem/accounts";
7
8
  import { TronWeb } from "tronweb";
@@ -33,6 +34,8 @@ export function decodeResponse(value) {
33
34
  return decodePaymentResponseHeader(value);
34
35
  }
35
36
  export function ensurePermit2(requirement) {
37
+ if (requirement.scheme !== "exact")
38
+ return requirement;
36
39
  const extra = { ...(requirement.extra ?? {}) };
37
40
  if (!extra.assetTransferMethod) {
38
41
  const token = findTokenByAddress(requirement.network, requirement.asset);
@@ -40,7 +43,7 @@ export function ensurePermit2(requirement) {
40
43
  extra.assetTransferMethod = "permit2";
41
44
  }
42
45
  }
43
- return { ...requirement, scheme: "exact", extra };
46
+ return { ...requirement, extra };
44
47
  }
45
48
  export function paymentRequired(requirement, resource, extensions) {
46
49
  return {
@@ -131,6 +134,8 @@ export async function createPaymentPayload(args) {
131
134
  const selected = ensurePermit2(args.selected);
132
135
  const required = paymentRequired(selected, args.resource, args.extensions);
133
136
  if (selected.network.startsWith("eip155:")) {
137
+ if (selected.scheme !== "exact")
138
+ throw new Error(`unsupported scheme ${selected.scheme} on ${selected.network}`);
134
139
  const privateKey = privateKeyFrom(["EVM_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["evm_client", "payer", "default"]);
135
140
  const account = privateKeyToAccount(privateKey);
136
141
  const rpcUrl = evmRpcUrl(selected.network, args.rpcUrl);
@@ -150,9 +155,23 @@ export async function createPaymentPayload(args) {
150
155
  apiKey: args.apiKey || process.env.TRON_GRID_API_KEY,
151
156
  allowanceMode: args.allowanceMode || process.env.X402_TRON_ALLOWANCE_MODE || "auto",
152
157
  });
153
- return new x402Client()
154
- .register(selected.network, new ExactTronScheme(signer))
155
- .createPaymentPayload(required);
158
+ const client = new x402Client();
159
+ if (selected.scheme === "exact_gasfree") {
160
+ registerExactGasFreeTronScheme(client, {
161
+ signer,
162
+ networks: [selected.network],
163
+ ...(args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL
164
+ ? { schemeOptions: { apiBaseUrls: { [selected.network]: args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL } } }
165
+ : {}),
166
+ });
167
+ }
168
+ else if (selected.scheme === "exact") {
169
+ client.register(selected.network, new ExactTronScheme(signer));
170
+ }
171
+ else {
172
+ throw new Error(`unsupported scheme ${selected.scheme} on ${selected.network}`);
173
+ }
174
+ return client.createPaymentPayload(required);
156
175
  }
157
176
  throw new Error(`unsupported network ${selected.network}`);
158
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bankofai/x402-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1-beta.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -20,10 +20,10 @@
20
20
  "node": ">=20"
21
21
  },
22
22
  "dependencies": {
23
- "@bankofai/x402-core": "1.0.0",
24
- "@bankofai/x402-evm": "1.0.0",
23
+ "@bankofai/x402-core": "1.0.1-beta.2",
24
+ "@bankofai/x402-evm": "1.0.1-beta.2",
25
25
  "@bankofai/x402-gateway": "^1.0.0",
26
- "@bankofai/x402-tron": "1.0.0",
26
+ "@bankofai/x402-tron": "1.0.1-beta.2",
27
27
  "tronweb": "6.4.0",
28
28
  "viem": "^2.55.0",
29
29
  "yaml": "^2.8.2"