@bankofai/x402-cli 1.0.0-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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BankofAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # x402-cli
2
+
3
+ TypeScript command-line client for BankofAI x402 payments. This version uses
4
+ the npm TypeScript SDK packages only:
5
+
6
+ - `@bankofai/x402-core@1.0.0`
7
+ - `@bankofai/x402-evm@1.0.0`
8
+ - `@bankofai/x402-tron@1.0.0`
9
+
10
+ Stablecoin payments use `scheme=exact` with
11
+ `extra.assetTransferMethod=permit2`.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install
17
+ npm run build
18
+ ```
19
+
20
+ Run from source during development:
21
+
22
+ ```bash
23
+ npm run dev -- serve --pay-to <recipient> --amount 0.0001 --network tron:nile --token USDT
24
+ ```
25
+
26
+ Run the compiled CLI:
27
+
28
+ ```bash
29
+ node dist/cli.js <command> [options]
30
+ ```
31
+
32
+ ## Commands
33
+
34
+ ### Serve
35
+
36
+ Start a local x402 paywall endpoint:
37
+
38
+ ```bash
39
+ node dist/cli.js serve \
40
+ --pay-to <recipient> \
41
+ --amount 0.0001 \
42
+ --network tron:nile \
43
+ --token USDT \
44
+ --port 4020
45
+ ```
46
+
47
+ The server exposes:
48
+
49
+ - `GET /health`
50
+ - `GET /.well-known/x402`
51
+ - `GET /pay` returns `402 Payment Required`
52
+ - `POST /pay` verifies and settles with the facilitator
53
+
54
+ ### Pay
55
+
56
+ Pay an x402-protected URL:
57
+
58
+ ```bash
59
+ TRON_PRIVATE_KEY=<hex> \
60
+ node dist/cli.js pay http://127.0.0.1:4020/pay \
61
+ --network tron:nile \
62
+ --token USDT
63
+ ```
64
+
65
+ For EVM networks use `EVM_PRIVATE_KEY` or `PRIVATE_KEY`.
66
+
67
+ ### Roundtrip
68
+
69
+ Start a temporary local server and immediately pay it:
70
+
71
+ ```bash
72
+ TRON_PRIVATE_KEY=<hex> \
73
+ node dist/cli.js roundtrip \
74
+ --pay-to <recipient> \
75
+ --amount 0.0001 \
76
+ --network tron:nile \
77
+ --token USDT
78
+ ```
79
+
80
+ ## Networks
81
+
82
+ Supported built-in token registry:
83
+
84
+ - `tron:mainnet` USDT, USDD
85
+ - `tron:nile` USDT, USDD
86
+ - `tron:shasta` USDT
87
+ - `eip155:56` USDT
88
+ - `eip155:97` USDT, USDC
89
+
90
+ Aliases accepted:
91
+
92
+ - `tron-mainnet` -> `tron:mainnet`
93
+ - `tron-nile` -> `tron:nile`
94
+ - `bsc-mainnet` -> `eip155:56`
95
+ - `bsc-testnet` -> `eip155:97`
96
+
97
+ ## Facilitator
98
+
99
+ Set a facilitator URL when needed:
100
+
101
+ ```bash
102
+ FACILITATOR_URL=https://facilitator.bankofai.io
103
+ ```
104
+
105
+ CLI payment challenges and payload selection always emit `scheme: "exact"` for
106
+ the SDK 1.0 Permit2 path.
package/dist/cli.js ADDED
@@ -0,0 +1,624 @@
1
+ #!/usr/bin/env node
2
+ import http from "node:http";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import YAML from "yaml";
8
+ import { createPaymentPayload, decodeRequired, decodeResponse, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
9
+ import { getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
10
+ function parseArgs(argv) {
11
+ const [command = "help", ...rest] = argv;
12
+ const positional = [];
13
+ const options = {};
14
+ for (let i = 0; i < rest.length; i += 1) {
15
+ const item = rest[i];
16
+ if (!item.startsWith("--")) {
17
+ positional.push(item);
18
+ continue;
19
+ }
20
+ const key = item.slice(2);
21
+ const next = rest[i + 1];
22
+ if (!next || next.startsWith("--")) {
23
+ options[key] = true;
24
+ }
25
+ else {
26
+ if (key === "header") {
27
+ const current = options[key];
28
+ options[key] = Array.isArray(current) ? [...current, next] : current ? [String(current), next] : [next];
29
+ }
30
+ else {
31
+ options[key] = next;
32
+ }
33
+ i += 1;
34
+ }
35
+ }
36
+ return { command, positional, options };
37
+ }
38
+ function opt(options, key, fallback) {
39
+ const value = options[key];
40
+ return typeof value === "string" ? value : fallback;
41
+ }
42
+ function optAll(options, key) {
43
+ const value = options[key];
44
+ if (Array.isArray(value))
45
+ return value;
46
+ return typeof value === "string" ? [value] : [];
47
+ }
48
+ function printJson(value) {
49
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
50
+ }
51
+ function readYaml(file) {
52
+ return YAML.parse(fs.readFileSync(file, "utf8"));
53
+ }
54
+ function expandEnv(value) {
55
+ return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? "");
56
+ }
57
+ function expandDeep(value) {
58
+ if (typeof value === "string")
59
+ return expandEnv(value);
60
+ if (Array.isArray(value))
61
+ return value.map(expandDeep);
62
+ if (value && typeof value === "object") {
63
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandDeep(item)]));
64
+ }
65
+ return value;
66
+ }
67
+ function providerFiles(root) {
68
+ const stat = fs.statSync(root);
69
+ if (stat.isFile())
70
+ return [root];
71
+ const out = [];
72
+ for (const entry of fs.readdirSync(root, { recursive: true })) {
73
+ const file = path.join(root, String(entry));
74
+ if (file.endsWith("provider.yml") || file.endsWith("provider.yaml"))
75
+ out.push(file);
76
+ }
77
+ return out.sort();
78
+ }
79
+ function loadProviderFile(file) {
80
+ const provider = expandDeep(readYaml(file));
81
+ validateProvider(provider, file);
82
+ provider.operator.network = normalizeNetwork(provider.operator.network);
83
+ provider.operator.scheme = "exact";
84
+ return provider;
85
+ }
86
+ function validateProvider(provider, file = "provider.yml") {
87
+ const required = [
88
+ ["name", provider?.name],
89
+ ["forward_url", provider?.forward_url],
90
+ ["operator.network", provider?.operator?.network],
91
+ ["operator.recipient", provider?.operator?.recipient],
92
+ ];
93
+ for (const [name, value] of required) {
94
+ if (typeof value !== "string" || !value.trim())
95
+ throw new Error(`${file}: ${name} is required`);
96
+ }
97
+ if (!Array.isArray(provider.endpoints) || !provider.endpoints.length) {
98
+ throw new Error(`${file}: endpoints must contain at least one endpoint`);
99
+ }
100
+ const seen = new Set();
101
+ for (const endpoint of provider.endpoints) {
102
+ if (typeof endpoint.method !== "string" || typeof endpoint.path !== "string") {
103
+ throw new Error(`${file}: each endpoint needs method and path`);
104
+ }
105
+ const key = `${endpoint.method.toUpperCase()} ${endpoint.path}`;
106
+ if (seen.has(key))
107
+ throw new Error(`${file}: duplicate endpoint ${key}`);
108
+ seen.add(key);
109
+ }
110
+ }
111
+ function providerPrice(endpoint) {
112
+ return endpoint?.metering?.dimensions?.[0]?.tiers?.[0]?.price_usd ?? 0;
113
+ }
114
+ function providerAssetTransferMethod(provider) {
115
+ return provider.operator?.asset_transfer_method ?? provider.operator?.assetTransferMethod ?? "permit2";
116
+ }
117
+ function providerCatalog(provider) {
118
+ return {
119
+ name: provider.name,
120
+ title: provider.title ?? provider.name,
121
+ description: provider.description ?? "",
122
+ category: provider.category ?? "other",
123
+ service_url: provider.display?.service_url,
124
+ tags: provider.display?.tags ?? [],
125
+ network: normalizeNetwork(provider.operator.network),
126
+ currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
127
+ endpoints: (provider.endpoints ?? []).map((endpoint) => ({
128
+ method: endpoint.method.toUpperCase(),
129
+ path: `/providers/${provider.name}${endpoint.path}`,
130
+ upstream_path: endpoint.path,
131
+ description: endpoint.description ?? "",
132
+ paid: providerPrice(endpoint) > 0 ? {
133
+ scheme: "exact",
134
+ network: normalizeNetwork(provider.operator.network),
135
+ currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
136
+ price_usd: providerPrice(endpoint),
137
+ } : null,
138
+ x402_routes: providerPrice(endpoint) > 0 ? [{
139
+ provider: provider.name,
140
+ network: normalizeNetwork(provider.operator.network),
141
+ scheme: "exact",
142
+ assetTransferMethod: providerAssetTransferMethod(provider),
143
+ url: `/providers/${provider.name}${endpoint.path}`,
144
+ }] : [],
145
+ })),
146
+ };
147
+ }
148
+ async function readCatalog(source) {
149
+ const text = await readText(source);
150
+ const parsed = JSON.parse(text);
151
+ if (Array.isArray(parsed))
152
+ return parsed;
153
+ if (Array.isArray(parsed.providers))
154
+ return parsed.providers;
155
+ if (Array.isArray(parsed.items))
156
+ return parsed.items;
157
+ return [];
158
+ }
159
+ async function readText(source) {
160
+ if (!source.startsWith("http://") && !source.startsWith("https://")) {
161
+ return fs.readFileSync(source, "utf8");
162
+ }
163
+ const response = await fetch(source);
164
+ if (!response.ok)
165
+ throw new Error(`failed to fetch ${source}: ${response.status}`);
166
+ return response.text();
167
+ }
168
+ async function readJson(source) {
169
+ return JSON.parse(await readText(source));
170
+ }
171
+ function catalogDetailSource(source, section, name) {
172
+ if (source.startsWith("http://") || source.startsWith("https://")) {
173
+ const base = new URL(source);
174
+ const pathname = base.pathname.endsWith("/catalog.json")
175
+ ? base.pathname.slice(0, -"catalog.json".length)
176
+ : base.pathname.endsWith("/")
177
+ ? base.pathname
178
+ : `${base.pathname}/`;
179
+ base.pathname = `${pathname}${section}/${name}.json`;
180
+ base.search = "";
181
+ base.hash = "";
182
+ return base.toString();
183
+ }
184
+ const stat = fs.existsSync(source) ? fs.statSync(source) : undefined;
185
+ const root = stat?.isDirectory() ? source : path.dirname(source);
186
+ return path.join(root, section, `${name}.json`);
187
+ }
188
+ async function readCatalogProvider(source, name) {
189
+ const providers = await readCatalog(source);
190
+ const summary = providers.find((item) => item.name === name || item.fqn === name);
191
+ if (!summary)
192
+ throw new Error(`provider not found: ${name}`);
193
+ if (Array.isArray(summary.endpoints) && summary.endpoints.length)
194
+ return summary;
195
+ const fqn = summary.fqn ?? summary.name ?? name;
196
+ try {
197
+ return await readJson(catalogDetailSource(source, "providers", fqn));
198
+ }
199
+ catch {
200
+ return summary;
201
+ }
202
+ }
203
+ async function readCatalogPayProvider(source, name) {
204
+ const providers = await readCatalog(source);
205
+ const summary = providers.find((item) => item.name === name || item.fqn === name);
206
+ const fqn = summary?.fqn ?? summary?.name ?? name;
207
+ try {
208
+ return await readJson(catalogDetailSource(source, "pay", fqn));
209
+ }
210
+ catch {
211
+ if (summary)
212
+ return readCatalogProvider(source, name);
213
+ throw new Error(`provider not found: ${name}`);
214
+ }
215
+ }
216
+ function buildRequirement(options) {
217
+ const network = normalizeNetwork(opt(options, "network", "tron:nile"));
218
+ 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);
221
+ return {
222
+ scheme: "exact",
223
+ network,
224
+ amount,
225
+ asset: opt(options, "asset") ?? token.address,
226
+ payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
227
+ maxTimeoutSeconds: Number(opt(options, "valid-for-seconds", "300")),
228
+ extra: token.assetTransferMethod ? { assetTransferMethod: token.assetTransferMethod } : {},
229
+ };
230
+ }
231
+ async function facilitatorPost(baseUrl, path, body) {
232
+ const response = await fetch(new URL(path, baseUrl), {
233
+ method: "POST",
234
+ headers: { "content-type": "application/json" },
235
+ body: JSON.stringify(body),
236
+ });
237
+ const text = await response.text();
238
+ const data = text ? JSON.parse(text) : {};
239
+ if (!response.ok)
240
+ throw new Error(`facilitator ${path} failed: ${response.status} ${text}`);
241
+ return data;
242
+ }
243
+ function requestHeaders(options) {
244
+ const headersOut = new Headers();
245
+ for (const header of optAll(options, "header")) {
246
+ const idx = header.indexOf(":");
247
+ if (idx <= 0)
248
+ throw new Error(`invalid --header '${header}', expected 'Name: Value'`);
249
+ headersOut.set(header.slice(0, idx).trim(), header.slice(idx + 1).trim());
250
+ }
251
+ return headersOut;
252
+ }
253
+ function validateAmountLimits(selected, options) {
254
+ const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
255
+ const maxAmount = opt(options, "max-amount");
256
+ if (maxRaw && BigInt(selected.amount) > BigInt(maxRaw)) {
257
+ throw new Error(`payment raw amount ${selected.amount} exceeds --max-rawAmount ${maxRaw}`);
258
+ }
259
+ if (maxAmount) {
260
+ const token = getToken(selected.network, opt(options, "token", "USDT"));
261
+ if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, token.decimals))) {
262
+ throw new Error(`payment amount exceeds --max-amount ${maxAmount}`);
263
+ }
264
+ }
265
+ }
266
+ async function serve(options) {
267
+ const host = opt(options, "host", "127.0.0.1");
268
+ const port = Number(opt(options, "port", "4020"));
269
+ const facilitatorUrl = opt(options, "facilitator-url", process.env.FACILITATOR_URL ?? "https://facilitator.bankofai.io");
270
+ const requirement = buildRequirement(options);
271
+ if (!requirement.payTo)
272
+ throw new Error("--pay-to is required");
273
+ const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
274
+ const challenge = {
275
+ x402Version: 2,
276
+ error: "Payment required",
277
+ resource: { url: resourceUrl },
278
+ accepts: [requirement],
279
+ };
280
+ const server = http.createServer(async (request, response) => {
281
+ try {
282
+ if (request.url?.startsWith("/health")) {
283
+ response.writeHead(200, { "content-type": "application/json" });
284
+ response.end(JSON.stringify({ ok: true }));
285
+ return;
286
+ }
287
+ if (request.url?.startsWith("/.well-known/x402")) {
288
+ response.writeHead(200, { "content-type": "application/json" });
289
+ response.end(JSON.stringify({ network: requirement.network, scheme: "exact", asset: requirement.asset, amount: requirement.amount, payTo: requirement.payTo, pay_url: resourceUrl }));
290
+ return;
291
+ }
292
+ if (!request.url?.startsWith("/pay")) {
293
+ response.writeHead(404).end("not found");
294
+ return;
295
+ }
296
+ const signature = request.headers[headers.signature.toLowerCase()];
297
+ if (!signature || Array.isArray(signature)) {
298
+ response.writeHead(402, {
299
+ "content-type": "application/json",
300
+ [headers.required]: encodeRequired(challenge),
301
+ });
302
+ response.end(JSON.stringify(challenge));
303
+ return;
304
+ }
305
+ const payload = Buffer.from(signature, "base64").toString("utf8").startsWith("{")
306
+ ? JSON.parse(Buffer.from(signature, "base64").toString("utf8"))
307
+ : signature;
308
+ const verify = await facilitatorPost(facilitatorUrl, "/verify", {
309
+ paymentPayload: payload,
310
+ paymentRequirements: requirement,
311
+ });
312
+ if (verify?.valid === false || verify?.isValid === false) {
313
+ response.writeHead(400, { "content-type": "application/json" });
314
+ response.end(JSON.stringify({ error: "payment verification failed", verify }));
315
+ return;
316
+ }
317
+ const settle = await facilitatorPost(facilitatorUrl, "/settle", {
318
+ paymentPayload: payload,
319
+ paymentRequirements: requirement,
320
+ });
321
+ response.writeHead(200, {
322
+ "content-type": "application/json",
323
+ [headers.response]: encodeResponse(settle),
324
+ });
325
+ response.end(JSON.stringify({ success: true, network: requirement.network, scheme: "exact", transaction: settle.transaction ?? settle.txHash ?? null }));
326
+ }
327
+ catch (error) {
328
+ response.writeHead(500, { "content-type": "application/json" });
329
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
330
+ }
331
+ });
332
+ 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 } });
334
+ });
335
+ }
336
+ function selectRequirement(accepts, options) {
337
+ const network = opt(options, "network");
338
+ const scheme = opt(options, "scheme");
339
+ const token = opt(options, "token");
340
+ const selected = accepts.find(req => {
341
+ if (network && normalizeNetwork(network) !== req.network)
342
+ return false;
343
+ if (scheme && scheme !== req.scheme)
344
+ return false;
345
+ if (token) {
346
+ const tokenInfo = getToken(req.network, token);
347
+ if (tokenInfo.address.toLowerCase() !== req.asset.toLowerCase())
348
+ return false;
349
+ }
350
+ return true;
351
+ });
352
+ if (!selected)
353
+ throw new Error("no matching payment requirement");
354
+ return selected;
355
+ }
356
+ async function pay(url, options) {
357
+ const method = opt(options, "method", "GET");
358
+ const baseHeaders = requestHeaders(options);
359
+ const probe = await fetch(url, {
360
+ method,
361
+ headers: baseHeaders,
362
+ body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : opt(options, "body"),
363
+ });
364
+ if (probe.status !== 402) {
365
+ printJson({ ok: true, command: "client", result: { status: probe.status, body: await probe.text() } });
366
+ return;
367
+ }
368
+ const header = probe.headers.get(headers.required);
369
+ if (!header)
370
+ throw new Error("402 response missing PAYMENT-REQUIRED header");
371
+ const required = decodeRequired(header);
372
+ const selected = selectRequirement(required.accepts ?? [], options);
373
+ validateAmountLimits(selected, options);
374
+ if (options["dry-run"]) {
375
+ printJson({
376
+ ok: true,
377
+ command: "client",
378
+ network: selected.network,
379
+ scheme: "exact",
380
+ result: {
381
+ url,
382
+ resource: required.resource?.url ?? url,
383
+ selected,
384
+ message: "Dry run - no payment submitted",
385
+ },
386
+ });
387
+ return;
388
+ }
389
+ const payload = await createPaymentPayload({
390
+ selected,
391
+ resource: required.resource?.url ?? url,
392
+ extensions: required.extensions,
393
+ rpcUrl: opt(options, "rpc-url"),
394
+ privateKey: opt(options, "private-key"),
395
+ });
396
+ const retryHeaders = new Headers(baseHeaders);
397
+ retryHeaders.set(headers.signature, encodeSignature(payload));
398
+ const paid = await fetch(url, {
399
+ method,
400
+ headers: retryHeaders,
401
+ body: opt(options, "body"),
402
+ });
403
+ const text = await paid.text();
404
+ const paymentResponse = paid.headers.get(headers.response);
405
+ printJson({
406
+ ok: paid.ok,
407
+ command: "client",
408
+ network: selected.network,
409
+ scheme: "exact",
410
+ result: {
411
+ status: paid.status,
412
+ body: text,
413
+ ...(paymentResponse ? { paymentResponse: decodeResponse(paymentResponse) } : {}),
414
+ },
415
+ });
416
+ }
417
+ async function roundtrip(options) {
418
+ const port = Number(opt(options, "port", "4020"));
419
+ serve(options);
420
+ await delay(500);
421
+ await pay(`http://127.0.0.1:${port}/pay`, options);
422
+ process.exit(0);
423
+ }
424
+ function gatewayBinary() {
425
+ return process.env.X402_GATEWAY_BIN ||
426
+ path.resolve(process.cwd(), "../x402-gateway/dist/cli.js");
427
+ }
428
+ async function gatewayStart(args) {
429
+ const child = spawn(process.execPath, [gatewayBinary(), ...args], {
430
+ stdio: "inherit",
431
+ env: process.env,
432
+ });
433
+ await new Promise((resolve, reject) => {
434
+ child.on("exit", code => code === 0 ? resolve() : reject(new Error(`gateway exited with ${code}`)));
435
+ child.on("error", reject);
436
+ });
437
+ }
438
+ function gatewayCheck(target) {
439
+ const files = providerFiles(target);
440
+ const providers = files.map(loadProviderFile);
441
+ const names = new Set();
442
+ for (const provider of providers) {
443
+ if (names.has(provider.name))
444
+ throw new Error(`duplicate provider name: ${provider.name}`);
445
+ names.add(provider.name);
446
+ }
447
+ printJson({ ok: true, providers: providers.map(p => p.name), count: providers.length });
448
+ }
449
+ function gatewayScaffold(name, options) {
450
+ const outputDir = opt(options, "output-dir", path.join("providers", name));
451
+ const forwardUrl = opt(options, "forward-url", "https://api.example.com");
452
+ fs.mkdirSync(outputDir, { recursive: true });
453
+ const body = `name: ${name}
454
+ title: "${name}"
455
+ description: "x402 provider"
456
+ category: data
457
+ version: v1
458
+
459
+ forward_url: ${forwardUrl}
460
+
461
+ routing:
462
+ type: proxy
463
+
464
+ operator:
465
+ network: tron-nile
466
+ currencies:
467
+ usd: ["USDT"]
468
+ recipient: \${X402_PROVIDER_RECIPIENT_TRON}
469
+ scheme: exact
470
+ protocol: exact
471
+ asset_transfer_method: permit2
472
+ facilitator_url: \${X402_FACILITATOR_URL}
473
+ facilitator_api_key: \${X402_FACILITATOR_API_KEY}
474
+ valid_for_seconds: 300
475
+
476
+ endpoints:
477
+ - method: GET
478
+ path: /v1/ping
479
+ metering:
480
+ dimensions:
481
+ - tiers:
482
+ - price_usd: 0.0001
483
+ `;
484
+ fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
485
+ printJson({ ok: true, file: path.join(outputDir, "provider.yml") });
486
+ }
487
+ function catalogBuild(target, options) {
488
+ const providers = providerFiles(target).map(loadProviderFile).map(providerCatalog);
489
+ const catalog = { version: 1, generatedAt: new Date().toISOString(), providers };
490
+ const output = opt(options, "output", opt(options, "dist-dir") ? path.join(opt(options, "dist-dir"), "catalog.json") : undefined);
491
+ if (output) {
492
+ fs.mkdirSync(path.dirname(output), { recursive: true });
493
+ fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
494
+ printJson({ ok: true, output, count: providers.length });
495
+ }
496
+ else {
497
+ printJson(catalog);
498
+ }
499
+ }
500
+ function catalogPayAssets(target) {
501
+ const rows = providerFiles(target).map(loadProviderFile).flatMap(provider => (provider.endpoints ?? []).map((endpoint) => ({
502
+ provider: provider.name,
503
+ method: endpoint.method,
504
+ path: `/providers/${provider.name}${endpoint.path}`,
505
+ network: normalizeNetwork(provider.operator.network),
506
+ currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
507
+ price_usd: providerPrice(endpoint),
508
+ scheme: "exact",
509
+ assetTransferMethod: providerAssetTransferMethod(provider),
510
+ })));
511
+ printJson({ assets: rows, count: rows.length });
512
+ }
513
+ async function catalogSearch(source, query, options) {
514
+ 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)
537
+ .slice(0, Number(opt(options, "limit", "10")));
538
+ printJson({ query, count: hits.length, results: hits.map(item => item.provider) });
539
+ }
540
+ async function catalogShow(source, name) {
541
+ printJson(await readCatalogProvider(source, name));
542
+ }
543
+ async function catalogEndpoints(source, name) {
544
+ const provider = await readCatalogProvider(source, name);
545
+ printJson({ provider: provider.fqn ?? provider.name, endpoints: provider.endpoints ?? [] });
546
+ }
547
+ async function catalogPayJson(source, name) {
548
+ const provider = await readCatalogPayProvider(source, name);
549
+ const endpoint = (provider.endpoints ?? []).find((item) => item.paid || item.x402_routes?.length || item.x402Routes?.length) ?? provider.endpoints?.[0];
550
+ if (!endpoint)
551
+ throw new Error(`provider has no endpoints: ${name}`);
552
+ printJson({
553
+ provider: provider.fqn ?? provider.name,
554
+ url: endpoint.url ?? endpoint.path,
555
+ method: endpoint.method,
556
+ paid: endpoint.paid,
557
+ x402_routes: endpoint.x402_routes ?? endpoint.x402Routes ?? [],
558
+ endpoint,
559
+ });
560
+ }
561
+ async function handleGateway(args) {
562
+ 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")]);
565
+ else if (command === "check")
566
+ gatewayCheck(positional[0] ?? opt(options, "providers", "providers"));
567
+ else if (command === "scaffold")
568
+ gatewayScaffold(positional[0] ?? "example-provider", options);
569
+ else if (command === "catalog")
570
+ await handleGatewayCatalog(positional, options);
571
+ else
572
+ throw new Error("Usage: x402-cli gateway <start|check|scaffold|catalog>");
573
+ }
574
+ async function handleGatewayCatalog(positional, options) {
575
+ const sub = positional[0] ?? "build";
576
+ const target = positional[1] ?? opt(options, "providers", "providers");
577
+ if (sub === "build")
578
+ catalogBuild(target, options);
579
+ else if (sub === "check")
580
+ gatewayCheck(target);
581
+ else if (sub === "pay-assets")
582
+ catalogPayAssets(target);
583
+ else if (sub === "search")
584
+ await catalogSearch(opt(options, "catalog", target), positional.slice(2).join(" ") || opt(options, "query", ""), options);
585
+ else
586
+ throw new Error("Usage: x402-cli gateway catalog <build|check|pay-assets|search>");
587
+ }
588
+ async function handleCatalog(args) {
589
+ const { command, positional, options } = parseArgs(args);
590
+ const source = opt(options, "catalog", process.env.X402_CATALOG || "dist/catalog.json");
591
+ if (command === "search")
592
+ await catalogSearch(source, positional.join(" "), options);
593
+ else if (command === "show")
594
+ await catalogShow(source, positional[0]);
595
+ else if (command === "endpoints")
596
+ await catalogEndpoints(source, positional[0]);
597
+ else if (command === "pay-json")
598
+ await catalogPayJson(source, positional[0]);
599
+ else if (command === "build")
600
+ catalogBuild(positional[0] ?? "providers", options);
601
+ else
602
+ throw new Error("Usage: x402-cli catalog <search|show|endpoints|pay-json|build>");
603
+ }
604
+ async function main() {
605
+ const argv = process.argv.slice(2);
606
+ const { command, positional, options } = parseArgs(argv);
607
+ if (command === "serve")
608
+ await serve(options);
609
+ else if (command === "pay")
610
+ await pay(positional[0], options);
611
+ else if (command === "roundtrip")
612
+ await roundtrip(options);
613
+ else if (command === "gateway")
614
+ await handleGateway(argv.slice(1));
615
+ else if (command === "catalog")
616
+ await handleCatalog(argv.slice(1));
617
+ else {
618
+ process.stdout.write("Usage: x402-cli <serve|pay|roundtrip|gateway|catalog> [options]\n");
619
+ }
620
+ }
621
+ main().catch(error => {
622
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
623
+ process.exit(1);
624
+ });
package/dist/tokens.js ADDED
@@ -0,0 +1,99 @@
1
+ export const TOKENS = {
2
+ "tron:mainnet": {
3
+ USDT: {
4
+ address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
5
+ decimals: 6,
6
+ name: "Tether USD",
7
+ symbol: "USDT",
8
+ version: "1",
9
+ assetTransferMethod: "permit2",
10
+ },
11
+ USDD: {
12
+ address: "TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz",
13
+ decimals: 18,
14
+ name: "Decentralized USD",
15
+ symbol: "USDD",
16
+ version: "1",
17
+ assetTransferMethod: "permit2",
18
+ },
19
+ },
20
+ "tron:nile": {
21
+ USDT: {
22
+ address: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf",
23
+ decimals: 6,
24
+ name: "Tether USD",
25
+ symbol: "USDT",
26
+ version: "1",
27
+ assetTransferMethod: "permit2",
28
+ },
29
+ USDD: {
30
+ address: "TGjgvdTWWrybVLaVeFqSyVqJQWjxqRYbaK",
31
+ decimals: 18,
32
+ name: "Decentralized USD",
33
+ symbol: "USDD",
34
+ version: "1",
35
+ assetTransferMethod: "permit2",
36
+ },
37
+ },
38
+ "tron:shasta": {
39
+ USDT: {
40
+ address: "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs",
41
+ decimals: 6,
42
+ name: "Tether USD",
43
+ symbol: "USDT",
44
+ version: "1",
45
+ },
46
+ },
47
+ "eip155:56": {
48
+ USDT: {
49
+ address: "0x55d398326f99059fF775485246999027B3197955",
50
+ decimals: 18,
51
+ name: "Tether USD",
52
+ symbol: "USDT",
53
+ version: "1",
54
+ assetTransferMethod: "permit2",
55
+ },
56
+ },
57
+ "eip155:97": {
58
+ USDT: {
59
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
60
+ decimals: 18,
61
+ name: "USD Coin",
62
+ symbol: "USDT",
63
+ version: "1",
64
+ assetTransferMethod: "permit2",
65
+ },
66
+ USDC: {
67
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
68
+ decimals: 18,
69
+ name: "USD Coin",
70
+ symbol: "USDC",
71
+ version: "1",
72
+ assetTransferMethod: "permit2",
73
+ },
74
+ },
75
+ };
76
+ export function normalizeNetwork(network) {
77
+ return ({
78
+ "tron-mainnet": "tron:mainnet",
79
+ "tron-shasta": "tron:shasta",
80
+ "tron-nile": "tron:nile",
81
+ "bsc-mainnet": "eip155:56",
82
+ "bsc-testnet": "eip155:97",
83
+ }[network] ?? network);
84
+ }
85
+ export function getToken(network, symbol) {
86
+ const token = TOKENS[normalizeNetwork(network)]?.[symbol.toUpperCase()];
87
+ if (!token)
88
+ throw new Error(`unknown token ${symbol} on ${network}`);
89
+ return token;
90
+ }
91
+ export function findTokenByAddress(network, address) {
92
+ const lower = address.toLowerCase();
93
+ return Object.values(TOKENS[normalizeNetwork(network)] ?? {}).find(token => token.address.toLowerCase() === lower);
94
+ }
95
+ export function toSmallestUnit(amount, decimals) {
96
+ const [whole, fraction = ""] = amount.split(".");
97
+ const padded = (fraction + "0".repeat(decimals)).slice(0, decimals);
98
+ return (BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(padded || "0")).toString();
99
+ }
package/dist/x402.js ADDED
@@ -0,0 +1,151 @@
1
+ import { decodePaymentRequiredHeader, decodePaymentResponseHeader, decodePaymentSignatureHeader, encodePaymentRequiredHeader, encodePaymentResponseHeader, encodePaymentSignatureHeader, } from "@bankofai/x402-core/http";
2
+ import { x402Client } from "@bankofai/x402-core/client";
3
+ import { ExactEvmScheme, toClientEvmSigner } from "@bankofai/x402-evm";
4
+ import { ExactTronScheme, createClientTronSigner } from "@bankofai/x402-tron";
5
+ import { createPublicClient, http } from "viem";
6
+ import { privateKeyToAccount } from "viem/accounts";
7
+ import { TronWeb } from "tronweb";
8
+ import { findTokenByAddress } from "./tokens.js";
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ export const headers = {
13
+ required: "PAYMENT-REQUIRED",
14
+ signature: "PAYMENT-SIGNATURE",
15
+ response: "PAYMENT-RESPONSE",
16
+ };
17
+ export function encodeRequired(value) {
18
+ return encodePaymentRequiredHeader(value);
19
+ }
20
+ export function encodeSignature(value) {
21
+ return encodePaymentSignatureHeader(value);
22
+ }
23
+ export function encodeResponse(value) {
24
+ return encodePaymentResponseHeader(value);
25
+ }
26
+ export function decodeRequired(value) {
27
+ return decodePaymentRequiredHeader(value);
28
+ }
29
+ export function decodeSignature(value) {
30
+ return decodePaymentSignatureHeader(value);
31
+ }
32
+ export function decodeResponse(value) {
33
+ return decodePaymentResponseHeader(value);
34
+ }
35
+ export function ensurePermit2(requirement) {
36
+ const extra = { ...(requirement.extra ?? {}) };
37
+ if (!extra.assetTransferMethod) {
38
+ const token = findTokenByAddress(requirement.network, requirement.asset);
39
+ if (token?.assetTransferMethod === "permit2") {
40
+ extra.assetTransferMethod = "permit2";
41
+ }
42
+ }
43
+ return { ...requirement, scheme: "exact", extra };
44
+ }
45
+ export function paymentRequired(requirement, resource, extensions) {
46
+ return {
47
+ x402Version: 2,
48
+ resource: { url: resource },
49
+ accepts: [ensurePermit2(requirement)],
50
+ ...(extensions ? { extensions } : {}),
51
+ };
52
+ }
53
+ function normalizePrivateKey(value) {
54
+ if (!value?.trim())
55
+ return undefined;
56
+ const trimmed = value.trim();
57
+ return (trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`);
58
+ }
59
+ function privateKeyFromAgentWallet(walletIds) {
60
+ const configPath = process.env.AGENT_WALLET_CONFIG ||
61
+ path.join(process.env.AGENT_WALLET_DIR || path.join(os.homedir(), ".agent-wallet"), "wallets_config.json");
62
+ if (!fs.existsSync(configPath))
63
+ return undefined;
64
+ try {
65
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
66
+ const wallets = config.wallets ?? {};
67
+ const ids = [
68
+ process.env.AGENT_WALLET_ID,
69
+ config.activeWalletId,
70
+ ...walletIds,
71
+ ...Object.keys(wallets),
72
+ ].filter(Boolean);
73
+ for (const id of ids) {
74
+ const wallet = wallets[String(id)];
75
+ const key = wallet?.params?.private_key ?? wallet?.material?.private_key ?? wallet?.private_key;
76
+ const normalized = normalizePrivateKey(key);
77
+ if (normalized)
78
+ return normalized;
79
+ }
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ return undefined;
85
+ }
86
+ function privateKeyFrom(names, explicit, walletIds) {
87
+ for (const value of [explicit, ...names.map(name => process.env[name])]) {
88
+ const normalized = normalizePrivateKey(value);
89
+ if (normalized)
90
+ return normalized;
91
+ }
92
+ const walletKey = privateKeyFromAgentWallet(walletIds);
93
+ if (walletKey)
94
+ return walletKey;
95
+ throw new Error(`missing private key; set one of ${names.join(", ")}`);
96
+ }
97
+ function evmRpcUrl(network, explicit) {
98
+ const chainId = network.split(":")[1];
99
+ return (explicit ||
100
+ process.env[`EVM_RPC_URL_${chainId}`] ||
101
+ process.env.RPC_URL ||
102
+ process.env.EVM_RPC_URL ||
103
+ (chainId === "56" ? "https://bsc-dataseed.binance.org" : undefined) ||
104
+ (chainId === "97" ? "https://data-seed-prebsc-1-s1.binance.org:8545" : undefined));
105
+ }
106
+ async function createTronWallet(privateKey) {
107
+ const rawKey = privateKey.replace(/^0x/, "");
108
+ const tronWeb = new TronWeb({ fullHost: "https://api.trongrid.io" });
109
+ const address = TronWeb.address.fromPrivateKey(rawKey);
110
+ if (!address)
111
+ throw new Error("invalid TRON private key");
112
+ return {
113
+ getAddress: () => address,
114
+ async signTypedData(args) {
115
+ const signature = await tronWeb.trx._signTypedData(args.domain, args.types, args.message, rawKey);
116
+ return signature.startsWith("0x") ? signature : `0x${signature}`;
117
+ },
118
+ async signTransaction(tx) {
119
+ return tronWeb.trx.sign(tx, rawKey);
120
+ },
121
+ };
122
+ }
123
+ export async function createPaymentPayload(args) {
124
+ const selected = ensurePermit2(args.selected);
125
+ const required = paymentRequired(selected, args.resource, args.extensions);
126
+ if (selected.network.startsWith("eip155:")) {
127
+ const privateKey = privateKeyFrom(["EVM_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["evm_client", "payer", "default"]);
128
+ const account = privateKeyToAccount(privateKey);
129
+ const rpcUrl = evmRpcUrl(selected.network, args.rpcUrl);
130
+ const publicClient = rpcUrl ? createPublicClient({ transport: http(rpcUrl) }) : undefined;
131
+ const signer = toClientEvmSigner(account, publicClient);
132
+ const scheme = new ExactEvmScheme(signer, rpcUrl ? { rpcUrl } : undefined);
133
+ return new x402Client()
134
+ .register(selected.network, scheme)
135
+ .createPaymentPayload(required);
136
+ }
137
+ if (selected.network.startsWith("tron:")) {
138
+ const privateKey = privateKeyFrom(["TRON_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["tron_client", "payer", "default"]);
139
+ const wallet = await createTronWallet(privateKey);
140
+ const signer = await createClientTronSigner(wallet, {
141
+ network: selected.network,
142
+ rpcUrl: args.rpcUrl || process.env.TRON_RPC_URL,
143
+ apiKey: args.apiKey || process.env.TRON_GRID_API_KEY,
144
+ allowanceMode: args.allowanceMode || process.env.X402_TRON_ALLOWANCE_MODE || "auto",
145
+ });
146
+ return new x402Client()
147
+ .register(selected.network, new ExactTronScheme(signer))
148
+ .createPaymentPayload(required);
149
+ }
150
+ throw new Error(`unsupported network ${selected.network}`);
151
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@bankofai/x402-cli",
3
+ "version": "1.0.0-beta.0",
4
+ "type": "module",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "bin": {
9
+ "x402-cli": "dist/cli.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "start": "node dist/cli.js",
14
+ "dev": "tsx src/cli.ts"
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "dependencies": {
20
+ "@bankofai/x402-core": "1.0.0",
21
+ "@bankofai/x402-evm": "1.0.0",
22
+ "@bankofai/x402-tron": "1.0.0",
23
+ "tronweb": "^6.0.4",
24
+ "viem": "^2.40.3",
25
+ "yaml": "^2.8.2"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^24.10.1",
29
+ "tsx": "^4.20.6",
30
+ "typescript": "^5.9.3"
31
+ }
32
+ }