@bankofai/x402-cli 1.0.1-beta.5 → 1.0.1-beta.6

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
@@ -101,6 +101,24 @@ node dist/cli.js pay https://api.example.com/pay \
101
101
  Use `--gasfree-api-url <url>` or `X402_GASFREE_API_URL` to override the SDK's
102
102
  default relayer endpoint.
103
103
 
104
+ GasFree fees are separate from the advertised payment amount. Set a fee limit
105
+ so the CLI estimates the relayer fee and rejects the payment before signing if
106
+ the estimate is too high:
107
+
108
+ ```bash
109
+ x402-cli pay https://api.example.com/pay \
110
+ --scheme exact_gasfree \
111
+ --max-amount 0.01 \
112
+ --max-gasfree-fee 0.5 \
113
+ --json
114
+ ```
115
+
116
+ Use `--max-gasfree-fee-raw` to express the fee limit in the token's smallest
117
+ unit. Successful and failed paid responses distinguish `settled` (payment
118
+ completed) from `delivered` (HTTP business response succeeded). A settled
119
+ upstream failure has `paid=true`, `settled=true`, and `delivered=false` and
120
+ includes its transaction information.
121
+
104
122
  For EVM networks use `EVM_PRIVATE_KEY` or `PRIVATE_KEY`.
105
123
  Prefer environment variables over `--private-key` in shared environments,
106
124
  because command-line arguments may be visible to other local processes.
package/dist/cli.js CHANGED
@@ -321,6 +321,8 @@ Options:
321
321
  --token <symbol> Require a specific token
322
322
  --scheme <scheme> Require a specific x402 scheme
323
323
  --gasfree-api-url <url> Override the TRON GasFree relayer API URL
324
+ --max-gasfree-fee <amt> Maximum GasFree relayer fee in token units
325
+ --max-gasfree-fee-raw <n> Maximum GasFree relayer fee in smallest units
324
326
  --max-amount <amount> Maximum human-readable payment amount
325
327
  --max-raw-amount <amount> Maximum smallest-unit payment amount
326
328
  --dry-run Read requirements but do not sign or pay
@@ -1084,6 +1086,31 @@ function validateAmountLimits(selected, options) {
1084
1086
  }
1085
1087
  }
1086
1088
  }
1089
+ function gasfreeFeeLimitRaw(selected, options) {
1090
+ const maxRaw = opt(options, "max-gasfree-fee-raw");
1091
+ const maxHuman = opt(options, "max-gasfree-fee");
1092
+ if (maxRaw && maxHuman) {
1093
+ throw new CliError("INVALID_ARGUMENT", "--max-gasfree-fee and --max-gasfree-fee-raw are mutually exclusive", "Pass one GasFree fee limit.", 2);
1094
+ }
1095
+ if (selected.scheme !== "exact_gasfree") {
1096
+ if (maxRaw || maxHuman)
1097
+ throw new Error("GasFree fee limits require an exact_gasfree payment requirement");
1098
+ return undefined;
1099
+ }
1100
+ if (maxRaw)
1101
+ return assertRawAmount(maxRaw, "--max-gasfree-fee-raw");
1102
+ if (!maxHuman)
1103
+ return undefined;
1104
+ const token = findTokenByAddress(selected.network, selected.asset);
1105
+ const decimalsOption = opt(options, "decimals");
1106
+ if (!token && decimalsOption === undefined) {
1107
+ throw new Error("cannot evaluate --max-gasfree-fee for an unknown asset; pass --max-gasfree-fee-raw or --decimals");
1108
+ }
1109
+ const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
1110
+ if (!Number.isInteger(decimals) || decimals < 0)
1111
+ throw new Error("--decimals must be a non-negative integer");
1112
+ return toSmallestUnit(maxHuman, decimals);
1113
+ }
1087
1114
  async function serve(options) {
1088
1115
  const host = opt(options, "host", "127.0.0.1");
1089
1116
  const port = Number(opt(options, "port", "4020"));
@@ -1236,6 +1263,7 @@ async function pay(url, options) {
1236
1263
  const required = decodeRequired(header);
1237
1264
  const selected = selectRequirement(required.accepts ?? [], options);
1238
1265
  validateAmountLimits(selected, options);
1266
+ const maxGasfreeFeeRaw = gasfreeFeeLimitRaw(selected, options);
1239
1267
  if (options["dry-run"]) {
1240
1268
  emit({
1241
1269
  command: "client",
@@ -1251,16 +1279,17 @@ async function pay(url, options) {
1251
1279
  });
1252
1280
  return;
1253
1281
  }
1254
- const payload = await withSdkStdoutRedirect(outputMode(options) === "json", () => createPaymentPayload({
1282
+ const creation = await withSdkStdoutRedirect(outputMode(options) === "json", () => createPaymentPayload({
1255
1283
  selected,
1256
1284
  resource: required.resource?.url ?? url,
1257
1285
  extensions: required.extensions,
1258
1286
  rpcUrl: opt(options, "rpc-url"),
1259
1287
  privateKey: opt(options, "private-key"),
1260
1288
  gasfreeApiUrl: opt(options, "gasfree-api-url"),
1289
+ maxGasfreeFeeRaw,
1261
1290
  }));
1262
1291
  const retryHeaders = new Headers(baseHeaders);
1263
- retryHeaders.set(headers.signature, encodeSignature(payload));
1292
+ retryHeaders.set(headers.signature, encodeSignature(creation.payload));
1264
1293
  const paid = await fetchWithTimeout(url, {
1265
1294
  method,
1266
1295
  headers: retryHeaders,
@@ -1268,12 +1297,20 @@ async function pay(url, options) {
1268
1297
  }, timeoutMs(options), `fetch ${url}`);
1269
1298
  const body = await responsePayload(paid);
1270
1299
  const paymentResponse = paid.headers.get(headers.response);
1300
+ const settlement = paymentResponse ? decodeResponse(paymentResponse) : undefined;
1301
+ const settled = settlement !== undefined;
1271
1302
  const result = {
1272
1303
  url,
1273
1304
  status: paid.status,
1274
- paid: paid.ok,
1305
+ paid: settled,
1306
+ settled,
1307
+ delivered: paid.ok,
1275
1308
  response: body,
1276
- ...(paymentResponse ? { paymentResponse: decodeResponse(paymentResponse) } : {}),
1309
+ ...(settlement !== undefined ? {
1310
+ paymentResponse: settlement,
1311
+ transaction: settlement?.transaction ?? settlement?.txHash ?? null,
1312
+ } : {}),
1313
+ ...(creation.gasfreeEstimate ? { gasfreeEstimate: creation.gasfreeEstimate } : {}),
1277
1314
  };
1278
1315
  if (!paid.ok) {
1279
1316
  const retryAfter = paid.headers.get("retry-after");
package/dist/x402.js CHANGED
@@ -2,7 +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
+ import { ExactGasFreeTronScheme, createGasFreeApiClients, getGasFreeApiBaseUrl } from "@bankofai/x402-tron/gasfree";
6
6
  import { createPublicClient, http } from "viem";
7
7
  import { privateKeyToAccount } from "viem/accounts";
8
8
  import { TronWeb } from "tronweb";
@@ -142,9 +142,10 @@ export async function createPaymentPayload(args) {
142
142
  const publicClient = rpcUrl ? createPublicClient({ transport: http(rpcUrl) }) : undefined;
143
143
  const signer = toClientEvmSigner(account, publicClient);
144
144
  const scheme = new ExactEvmScheme(signer, rpcUrl ? { rpcUrl } : undefined);
145
- return new x402Client()
145
+ const payload = await new x402Client()
146
146
  .register(selected.network, scheme)
147
147
  .createPaymentPayload(required);
148
+ return { payload };
148
149
  }
149
150
  if (selected.network.startsWith("tron:")) {
150
151
  const privateKey = privateKeyFrom(["TRON_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["tron_client", "payer", "default"]);
@@ -156,14 +157,20 @@ export async function createPaymentPayload(args) {
156
157
  allowanceMode: args.allowanceMode || process.env.X402_TRON_ALLOWANCE_MODE || "auto",
157
158
  });
158
159
  const client = new x402Client();
160
+ let gasfreeEstimate;
159
161
  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
- : {}),
162
+ const apiUrl = args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL || getGasFreeApiBaseUrl(selected.network);
163
+ const scheme = new ExactGasFreeTronScheme(signer, {
164
+ apiClients: createGasFreeApiClients({ [selected.network]: apiUrl }),
166
165
  });
166
+ const total = await scheme.estimateCost(selected);
167
+ const amount = BigInt(selected.amount);
168
+ const fee = total - amount;
169
+ if (args.maxGasfreeFeeRaw !== undefined && fee > BigInt(args.maxGasfreeFeeRaw)) {
170
+ throw new Error(`estimated GasFree fee ${fee} exceeds --max-gasfree-fee limit ${args.maxGasfreeFeeRaw}`);
171
+ }
172
+ gasfreeEstimate = { fee: fee.toString(), total: total.toString() };
173
+ client.register(selected.network, scheme);
167
174
  }
168
175
  else if (selected.scheme === "exact") {
169
176
  client.register(selected.network, new ExactTronScheme(signer));
@@ -171,7 +178,8 @@ export async function createPaymentPayload(args) {
171
178
  else {
172
179
  throw new Error(`unsupported scheme ${selected.scheme} on ${selected.network}`);
173
180
  }
174
- return client.createPaymentPayload(required);
181
+ const payload = await client.createPaymentPayload(required);
182
+ return { payload, ...(gasfreeEstimate ? { gasfreeEstimate } : {}) };
175
183
  }
176
184
  throw new Error(`unsupported network ${selected.network}`);
177
185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bankofai/x402-cli",
3
- "version": "1.0.1-beta.5",
3
+ "version": "1.0.1-beta.6",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"