@bankofai/x402-gateway 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 BANK OF AI
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,107 @@
1
+ # x402 Gateway
2
+
3
+ TypeScript reverse proxy for paid HTTP APIs. This version uses the npm
4
+ TypeScript x402 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
+ Payment requirements are emitted as `scheme=exact`; supported stablecoins add
11
+ `extra.assetTransferMethod=permit2`.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install
17
+ npm run build
18
+ ```
19
+
20
+ ## Run
21
+
22
+ Start from a single provider file:
23
+
24
+ ```bash
25
+ node dist/cli.js --provider examples/provider.yml --host 127.0.0.1 --port 4020
26
+ ```
27
+
28
+ Start from a providers directory:
29
+
30
+ ```bash
31
+ node dist/cli.js --providers providers --host 0.0.0.0 --port 4020
32
+ ```
33
+
34
+ Health check:
35
+
36
+ ```bash
37
+ curl http://127.0.0.1:4020/__402/health
38
+ ```
39
+
40
+ Paid provider path:
41
+
42
+ ```bash
43
+ curl -i http://127.0.0.1:4020/providers/tron-nile-usdt/v1/ping
44
+ ```
45
+
46
+ If the endpoint has metering, the gateway returns `402 Payment Required` with a
47
+ `PAYMENT-REQUIRED` header. After the client retries with `PAYMENT-SIGNATURE`,
48
+ the gateway verifies and settles with the facilitator, then forwards to the
49
+ configured upstream.
50
+
51
+ ## Provider Config
52
+
53
+ Provider files stay in YAML:
54
+
55
+ ```yaml
56
+ name: tron-nile-usdt
57
+ forward_url: ${X402_PROVIDER_FORWARD_URL}
58
+
59
+ routing:
60
+ auth:
61
+ method: header
62
+ key: Authorization
63
+ prefix: "Bearer "
64
+ value_from_env: X402_PROVIDER_API_TOKEN
65
+
66
+ operator:
67
+ network: tron-nile
68
+ currencies:
69
+ usd: ["USDT"]
70
+ recipient: ${X402_PROVIDER_RECIPIENT_TRON}
71
+ scheme: exact
72
+ protocol: exact
73
+ asset_transfer_method: permit2
74
+ facilitator_url: ${X402_FACILITATOR_URL}
75
+ facilitator_api_key_env: X402_FACILITATOR_API_KEY
76
+ valid_for_seconds: 300
77
+
78
+ endpoints:
79
+ - method: GET
80
+ path: /v1/ping
81
+ metering:
82
+ dimensions:
83
+ - tiers:
84
+ - price_usd: 0.002
85
+ ```
86
+
87
+ `@bankofai/x402-*` 1.0.0 uses `scheme: exact` with
88
+ `extra.assetTransferMethod: permit2` in the payment requirement. Older provider
89
+ configs that say `exact_permit` are normalized at load time, but new provider
90
+ configs should use `protocol: exact` and `asset_transfer_method: permit2`.
91
+
92
+ Network aliases accepted:
93
+
94
+ - `tron-mainnet` -> `tron:mainnet`
95
+ - `tron-nile` -> `tron:nile`
96
+ - `bsc-mainnet` -> `eip155:56`
97
+ - `bsc-testnet` -> `eip155:97`
98
+
99
+ ## Environment
100
+
101
+ ```bash
102
+ X402_FACILITATOR_URL=https://facilitator.bankofai.io
103
+ X402_FACILITATOR_API_KEY=<optional-facilitator-api-key>
104
+ X402_PROVIDER_FORWARD_URL=<upstream-base-url>
105
+ X402_PROVIDER_RECIPIENT_TRON=<recipient-T-address>
106
+ X402_PROVIDER_API_TOKEN=<upstream-token>
107
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { loadProviders } from "./config.js";
3
+ import { createGatewayServer } from "./server.js";
4
+ function parseArgs(argv) {
5
+ const options = {};
6
+ for (let i = 0; i < argv.length; i += 1) {
7
+ const item = argv[i];
8
+ if (!item.startsWith("--"))
9
+ continue;
10
+ const key = item.slice(2);
11
+ const next = argv[i + 1];
12
+ if (!next || next.startsWith("--"))
13
+ options[key] = true;
14
+ else {
15
+ options[key] = next;
16
+ i += 1;
17
+ }
18
+ }
19
+ return options;
20
+ }
21
+ function opt(options, key, fallback) {
22
+ const value = options[key];
23
+ return typeof value === "string" ? value : fallback;
24
+ }
25
+ const options = parseArgs(process.argv.slice(2));
26
+ const providersPath = opt(options, "providers", opt(options, "provider", process.env.X402_GATEWAY_PROVIDERS_DIR || "/app/providers"));
27
+ const host = opt(options, "host", process.env.X402_GATEWAY_HOST || "0.0.0.0");
28
+ const port = Number(opt(options, "port", process.env.PORT || "8080"));
29
+ const providers = loadProviders(providersPath);
30
+ const server = createGatewayServer(providers);
31
+ server.listen(port, host, () => {
32
+ process.stdout.write(JSON.stringify({ ok: true, host, port, providers: [...providers.keys()] }, null, 2) + "\n");
33
+ });
package/dist/config.js ADDED
@@ -0,0 +1,136 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
5
+ function expandEnv(value) {
6
+ return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? "");
7
+ }
8
+ function expandDeep(value) {
9
+ if (typeof value === "string")
10
+ return expandEnv(value);
11
+ if (Array.isArray(value))
12
+ return value.map(expandDeep);
13
+ if (value && typeof value === "object") {
14
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandDeep(item)]));
15
+ }
16
+ return value;
17
+ }
18
+ function assertString(value, name) {
19
+ if (typeof value !== "string" || !value.trim())
20
+ throw new Error(`${name} is required`);
21
+ }
22
+ function validateProvider(config, file) {
23
+ assertString(config.name, `${file}: name`);
24
+ assertString(config.forward_url, `${file}: forward_url`);
25
+ assertString(config.operator?.network, `${file}: operator.network`);
26
+ assertString(config.operator?.recipient, `${file}: operator.recipient`);
27
+ if (!config.endpoints?.length)
28
+ throw new Error(`${file}: endpoints must contain at least one endpoint`);
29
+ const seen = new Set();
30
+ for (const [index, endpoint] of config.endpoints.entries()) {
31
+ assertString(endpoint.method, `${file}: endpoints[${index}].method`);
32
+ assertString(endpoint.path, `${file}: endpoints[${index}].path`);
33
+ if (!endpoint.path.startsWith("/"))
34
+ throw new Error(`${file}: endpoint path must start with /`);
35
+ const method = endpoint.method.toUpperCase();
36
+ if (!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].includes(method)) {
37
+ throw new Error(`${file}: unsupported endpoint method ${endpoint.method}`);
38
+ }
39
+ const key = `${method} ${endpoint.path}`;
40
+ if (seen.has(key))
41
+ throw new Error(`${file}: duplicate endpoint ${key}`);
42
+ seen.add(key);
43
+ for (const [tierIndex, tier] of (endpoint.metering?.dimensions?.[0]?.tiers ?? []).entries()) {
44
+ if (typeof tier.price_usd !== "number" || tier.price_usd < 0) {
45
+ throw new Error(`${file}: endpoints[${index}].metering tier ${tierIndex} price_usd must be >= 0`);
46
+ }
47
+ }
48
+ }
49
+ }
50
+ export function loadProvider(file) {
51
+ const config = expandDeep(YAML.parse(fs.readFileSync(file, "utf8")));
52
+ validateProvider(config, file);
53
+ config.operator.network = normalizeNetwork(config.operator.network);
54
+ normalizePaymentProtocol(config, file);
55
+ return {
56
+ config,
57
+ facilitatorUrl: config.operator.facilitator_url ||
58
+ process.env.X402_FACILITATOR_URL ||
59
+ process.env.FACILITATOR_URL ||
60
+ "https://facilitator.bankofai.io",
61
+ facilitatorApiKey: config.operator.facilitator_api_key ||
62
+ (config.operator.facilitator_api_key_env
63
+ ? process.env[config.operator.facilitator_api_key_env]
64
+ : undefined) ||
65
+ process.env.X402_FACILITATOR_API_KEY ||
66
+ process.env.FACILITATOR_API_KEY,
67
+ };
68
+ }
69
+ function normalizePaymentProtocol(config, file) {
70
+ const raw = String(config.operator.protocol || config.operator.scheme || "exact").toLowerCase();
71
+ const normalized = raw.replace(/[-:\s]/g, "_");
72
+ if (!["exact", "exact_permit", "permit2", "exact_permit2"].includes(normalized)) {
73
+ throw new Error(`${file}: unsupported x402 protocol ${raw}; use exact + permit2`);
74
+ }
75
+ config.operator.scheme = "exact";
76
+ config.operator.protocol = "exact";
77
+ config.operator.asset_transfer_method = "permit2";
78
+ config.operator.assetTransferMethod = "permit2";
79
+ }
80
+ export function loadProviders(providerPath) {
81
+ const stat = fs.statSync(providerPath);
82
+ const files = stat.isDirectory()
83
+ ? fs.readdirSync(providerPath, { recursive: true })
84
+ .map(item => path.join(providerPath, String(item)))
85
+ .filter(item => item.endsWith("provider.yml") || item.endsWith("provider.yaml"))
86
+ : [providerPath];
87
+ const entries = files.map(file => {
88
+ const entry = loadProvider(file);
89
+ return [entry.config.name, entry];
90
+ });
91
+ const names = new Set();
92
+ for (const [name] of entries) {
93
+ if (names.has(name))
94
+ throw new Error(`duplicate provider name: ${name}`);
95
+ names.add(name);
96
+ }
97
+ return new Map(entries);
98
+ }
99
+ export function endpointFor(provider, method, routePath) {
100
+ return provider.endpoints?.find(endpoint => endpoint.method.toUpperCase() === method.toUpperCase() && pathMatches(endpoint.path, routePath));
101
+ }
102
+ function pathMatches(template, routePath) {
103
+ const templateParts = template.split("/").filter(Boolean);
104
+ const routeParts = routePath.split("/").filter(Boolean);
105
+ if (templateParts.length !== routeParts.length)
106
+ return false;
107
+ return templateParts.every((part, index) => {
108
+ if (part.startsWith("{") && part.endsWith("}"))
109
+ return routeParts[index].length > 0;
110
+ return part === routeParts[index];
111
+ });
112
+ }
113
+ export function priceUsd(endpoint, params = {}) {
114
+ const variant = endpoint?.metering?.variants?.find(item => params[item.param] === item.value);
115
+ return (variant?.dimensions ?? endpoint?.metering?.dimensions)?.[0]?.tiers?.[0]?.price_usd ?? 0;
116
+ }
117
+ export function paymentRequirements(provider, price) {
118
+ if (price <= 0)
119
+ return [];
120
+ const network = normalizeNetwork(provider.operator.network);
121
+ const symbols = provider.operator.currencies?.usd ?? ["USDT"];
122
+ const payTo = provider.recipients?.[provider.operator.recipient]?.account ?? provider.operator.recipient;
123
+ return symbols.map(symbol => {
124
+ const token = getToken(network, symbol);
125
+ const transferMethod = provider.operator.assetTransferMethod || provider.operator.asset_transfer_method || token.assetTransferMethod;
126
+ return {
127
+ scheme: "exact",
128
+ network,
129
+ amount: toSmallestUnit(price, token.decimals),
130
+ asset: token.address,
131
+ payTo,
132
+ maxTimeoutSeconds: provider.operator.valid_for_seconds ?? 300,
133
+ extra: transferMethod === "permit2" ? { assetTransferMethod: "permit2" } : {},
134
+ };
135
+ });
136
+ }
package/dist/server.js ADDED
@@ -0,0 +1,249 @@
1
+ import http from "node:http";
2
+ import { URL } from "node:url";
3
+ import { endpointFor, paymentRequirements, priceUsd } from "./config.js";
4
+ import { decodeSignature, encodeRequired, encodeResponse, headers, matchRequirement } from "./x402.js";
5
+ const metrics = {
6
+ requests: 0,
7
+ paidRequests: 0,
8
+ verifyFailures: 0,
9
+ settleFailures: 0,
10
+ upstreamFailures: 0,
11
+ };
12
+ async function readBody(request) {
13
+ const chunks = [];
14
+ for await (const chunk of request)
15
+ chunks.push(Buffer.from(chunk));
16
+ return Buffer.concat(chunks);
17
+ }
18
+ function json(response, status, body, extraHeaders = {}) {
19
+ response.writeHead(status, { "content-type": "application/json", ...extraHeaders });
20
+ response.end(JSON.stringify(body));
21
+ }
22
+ async function facilitatorPost(entry, path, body) {
23
+ const headers = { "content-type": "application/json" };
24
+ if (entry.facilitatorApiKey) {
25
+ headers.authorization = `Bearer ${entry.facilitatorApiKey}`;
26
+ }
27
+ const response = await fetch(new URL(path, entry.facilitatorUrl), {
28
+ method: "POST",
29
+ headers,
30
+ body: JSON.stringify(body),
31
+ });
32
+ const text = await response.text();
33
+ const data = text ? JSON.parse(text) : {};
34
+ if (!response.ok)
35
+ throw new Error(`facilitator ${path} failed: ${response.status} ${text}`);
36
+ return data;
37
+ }
38
+ async function attachFeeQuotes(entry, requirements, context) {
39
+ try {
40
+ const quotes = await facilitatorPost(entry, "/fee_quote", {
41
+ paymentRequirements: requirements,
42
+ context,
43
+ });
44
+ const list = Array.isArray(quotes) ? quotes : quotes.quotes ?? quotes.fees ?? [];
45
+ return requirements.map(requirement => {
46
+ const quote = list.find((item) => item.scheme === requirement.scheme &&
47
+ item.network === requirement.network &&
48
+ String(item.asset).toLowerCase() === requirement.asset.toLowerCase());
49
+ return quote?.fee ? { ...requirement, extra: { ...requirement.extra, fee: quote.fee } } : requirement;
50
+ });
51
+ }
52
+ catch {
53
+ return requirements;
54
+ }
55
+ }
56
+ function isAdminAllowed(request) {
57
+ const token = process.env.X402_GATEWAY_ADMIN_TOKEN;
58
+ if (!token)
59
+ return true;
60
+ const auth = request.headers.authorization ?? "";
61
+ return auth === `Bearer ${token}`;
62
+ }
63
+ function requestParams(url, body, request) {
64
+ const params = {};
65
+ url.searchParams.forEach((value, key) => { params[key] = value; });
66
+ const contentType = String(request.headers["content-type"] ?? "").split(";", 1)[0].toLowerCase();
67
+ try {
68
+ if (body.length && contentType === "application/json") {
69
+ const parsed = JSON.parse(body.toString("utf8"));
70
+ if (parsed && typeof parsed === "object") {
71
+ for (const [key, value] of Object.entries(parsed)) {
72
+ if (["string", "number", "boolean"].includes(typeof value))
73
+ params[key] = String(value);
74
+ }
75
+ }
76
+ }
77
+ else if (body.length && contentType === "application/x-www-form-urlencoded") {
78
+ new URLSearchParams(body.toString("utf8")).forEach((value, key) => { params[key] = value; });
79
+ }
80
+ }
81
+ catch {
82
+ // Metering variants are advisory; malformed bodies just do not add params.
83
+ }
84
+ return params;
85
+ }
86
+ function upstreamHeaders(request, entry) {
87
+ const headersOut = new Headers();
88
+ for (const [key, value] of Object.entries(request.headers)) {
89
+ if (!value)
90
+ continue;
91
+ const lower = key.toLowerCase();
92
+ if (["host", "connection", "content-length", "authorization", "payment-signature"].includes(lower))
93
+ continue;
94
+ headersOut.set(key, Array.isArray(value) ? value.join(",") : value);
95
+ }
96
+ const auth = entry.config.routing?.auth;
97
+ if (auth) {
98
+ const value = auth.value ?? (auth.value_from_env ? process.env[auth.value_from_env] : undefined);
99
+ if (value && ["header", "access_token", "oauth2", undefined].includes(auth.method)) {
100
+ headersOut.set(auth.key ?? "Authorization", `${auth.prefix ?? ""}${value}`);
101
+ }
102
+ }
103
+ return headersOut;
104
+ }
105
+ function upstreamUrl(entry, request, routePath) {
106
+ const sourceUrl = new URL(request.url ?? "/", "http://local");
107
+ const upstream = new URL(routePath + (sourceUrl.search || ""), entry.config.forward_url);
108
+ const auth = entry.config.routing?.auth;
109
+ const value = auth?.value ?? (auth?.value_from_env ? process.env[auth.value_from_env] : undefined);
110
+ if (auth?.method === "query_param" && value) {
111
+ upstream.searchParams.set(auth.param ?? auth.key ?? "api_key", value);
112
+ }
113
+ return upstream;
114
+ }
115
+ async function forward(entry, request, response, routePath, body, paymentResponse) {
116
+ const upstream = upstreamUrl(entry, request, routePath);
117
+ const upstreamResponse = await fetch(upstream, {
118
+ method: request.method,
119
+ headers: upstreamHeaders(request, entry),
120
+ body: ["GET", "HEAD"].includes(request.method ?? "GET") ? undefined : new Uint8Array(body),
121
+ });
122
+ const responseHeaders = {};
123
+ upstreamResponse.headers.forEach((value, key) => {
124
+ if (!["connection", "transfer-encoding", "content-encoding", "content-length"].includes(key.toLowerCase())) {
125
+ responseHeaders[key] = value;
126
+ }
127
+ });
128
+ if (paymentResponse)
129
+ responseHeaders[headers.response] = encodeResponse(paymentResponse);
130
+ response.writeHead(upstreamResponse.status, responseHeaders);
131
+ response.end(Buffer.from(await upstreamResponse.arrayBuffer()));
132
+ }
133
+ export function createGatewayServer(providers) {
134
+ return http.createServer(async (request, response) => {
135
+ try {
136
+ metrics.requests += 1;
137
+ const url = new URL(request.url ?? "/", "http://local");
138
+ if (url.pathname === "/__402/health") {
139
+ json(response, 200, { ok: true, providers: providers.size });
140
+ return;
141
+ }
142
+ if (url.pathname === "/__402/ready") {
143
+ json(response, 200, { ok: true, providers: providers.size });
144
+ return;
145
+ }
146
+ if (url.pathname === "/__402/providers") {
147
+ if (!isAdminAllowed(request))
148
+ return json(response, 401, { error: "unauthorized" });
149
+ json(response, 200, { providers: [...providers.values()].map(entry => ({
150
+ name: entry.config.name,
151
+ title: entry.config.title,
152
+ network: entry.config.operator.network,
153
+ facilitatorUrl: entry.facilitatorUrl,
154
+ endpoints: entry.config.endpoints?.length ?? 0,
155
+ })) });
156
+ return;
157
+ }
158
+ if (url.pathname === "/__402/endpoints") {
159
+ if (!isAdminAllowed(request))
160
+ return json(response, 401, { error: "unauthorized" });
161
+ json(response, 200, { endpoints: [...providers.values()].flatMap(entry => (entry.config.endpoints ?? []).map(endpoint => ({
162
+ provider: entry.config.name,
163
+ method: endpoint.method,
164
+ path: endpoint.path,
165
+ priceUsd: priceUsd(endpoint),
166
+ network: entry.config.operator.network,
167
+ }))) });
168
+ return;
169
+ }
170
+ if (url.pathname === "/metrics") {
171
+ response.writeHead(200, { "content-type": "text/plain; version=0.0.4" });
172
+ response.end([
173
+ `x402_gateway_requests_total ${metrics.requests}`,
174
+ `x402_gateway_paid_requests_total ${metrics.paidRequests}`,
175
+ `x402_gateway_verify_failures_total ${metrics.verifyFailures}`,
176
+ `x402_gateway_settle_failures_total ${metrics.settleFailures}`,
177
+ `x402_gateway_upstream_failures_total ${metrics.upstreamFailures}`,
178
+ "",
179
+ ].join("\n"));
180
+ return;
181
+ }
182
+ const match = url.pathname.match(/^\/providers\/([^/]+)(\/.*)$/);
183
+ if (!match) {
184
+ json(response, 404, { error: "not found" });
185
+ return;
186
+ }
187
+ const entry = providers.get(match[1]);
188
+ if (!entry) {
189
+ json(response, 404, { error: "provider not found" });
190
+ return;
191
+ }
192
+ const routePath = match[2];
193
+ const endpoint = endpointFor(entry.config, request.method ?? "GET", routePath);
194
+ if (!endpoint) {
195
+ json(response, 404, { error: "endpoint not found" });
196
+ return;
197
+ }
198
+ const body = await readBody(request);
199
+ const requirements = paymentRequirements(entry.config, priceUsd(endpoint, requestParams(url, body, request)));
200
+ if (!requirements.length) {
201
+ await forward(entry, request, response, routePath, body);
202
+ return;
203
+ }
204
+ const paymentHeader = request.headers[headers.signature.toLowerCase()];
205
+ if (!paymentHeader || Array.isArray(paymentHeader)) {
206
+ const accepts = await attachFeeQuotes(entry, requirements);
207
+ const challenge = {
208
+ x402Version: 2,
209
+ error: "Payment required",
210
+ resource: { url: url.pathname },
211
+ accepts,
212
+ };
213
+ json(response, 402, challenge, { [headers.required]: encodeRequired(challenge) });
214
+ return;
215
+ }
216
+ const payload = decodeSignature(paymentHeader);
217
+ const requirement = requirements.find(item => matchRequirement(payload, item));
218
+ if (!requirement) {
219
+ json(response, 400, { error: "payment does not match any requirement" });
220
+ return;
221
+ }
222
+ const verify = await facilitatorPost(entry, "/verify", {
223
+ paymentPayload: payload,
224
+ paymentRequirements: requirement,
225
+ });
226
+ if (verify?.valid === false || verify?.isValid === false) {
227
+ metrics.verifyFailures += 1;
228
+ json(response, 400, { error: "payment verification failed", verify });
229
+ return;
230
+ }
231
+ let settle;
232
+ try {
233
+ settle = await facilitatorPost(entry, "/settle", {
234
+ paymentPayload: payload,
235
+ paymentRequirements: requirement,
236
+ });
237
+ }
238
+ catch (error) {
239
+ metrics.settleFailures += 1;
240
+ throw error;
241
+ }
242
+ metrics.paidRequests += 1;
243
+ await forward(entry, request, response, routePath, body, settle);
244
+ }
245
+ catch (error) {
246
+ json(response, 500, { error: error instanceof Error ? error.message : String(error) });
247
+ }
248
+ });
249
+ }
package/dist/tokens.js ADDED
@@ -0,0 +1,38 @@
1
+ export const TOKENS = {
2
+ "tron:mainnet": {
3
+ USDT: { address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", decimals: 6, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
4
+ USDD: { address: "TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz", decimals: 18, name: "Decentralized USD", symbol: "USDD", assetTransferMethod: "permit2" },
5
+ },
6
+ "tron:nile": {
7
+ USDT: { address: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", decimals: 6, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
8
+ USDD: { address: "TGjgvdTWWrybVLaVeFqSyVqJQWjxqRYbaK", decimals: 18, name: "Decentralized USD", symbol: "USDD", assetTransferMethod: "permit2" },
9
+ },
10
+ "eip155:56": {
11
+ USDT: { address: "0x55d398326f99059fF775485246999027B3197955", decimals: 18, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
12
+ },
13
+ "eip155:97": {
14
+ USDT: { address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd", decimals: 18, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
15
+ USDC: { address: "0x64544969ed7EBf5f083679233325356EbE738930", decimals: 18, name: "USD Coin", symbol: "USDC", assetTransferMethod: "permit2" },
16
+ },
17
+ };
18
+ export function normalizeNetwork(network) {
19
+ return ({
20
+ "tron-mainnet": "tron:mainnet",
21
+ "tron-shasta": "tron:shasta",
22
+ "tron-nile": "tron:nile",
23
+ "bsc-mainnet": "eip155:56",
24
+ "bsc-testnet": "eip155:97",
25
+ }[network] ?? network);
26
+ }
27
+ export function getToken(network, symbol) {
28
+ const normalized = normalizeNetwork(network);
29
+ const token = TOKENS[normalized]?.[symbol.toUpperCase()];
30
+ if (!token)
31
+ throw new Error(`unknown token ${symbol} on ${network}`);
32
+ return token;
33
+ }
34
+ export function toSmallestUnit(amount, decimals) {
35
+ const [whole, fraction = ""] = String(amount).split(".");
36
+ const padded = (fraction + "0".repeat(decimals)).slice(0, decimals);
37
+ return (BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(padded || "0")).toString();
38
+ }
package/dist/x402.js ADDED
@@ -0,0 +1,25 @@
1
+ import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, encodePaymentResponseHeader, } from "@bankofai/x402-core/http";
2
+ export const headers = {
3
+ required: "PAYMENT-REQUIRED",
4
+ signature: "PAYMENT-SIGNATURE",
5
+ response: "PAYMENT-RESPONSE",
6
+ };
7
+ export function encodeRequired(value) {
8
+ return encodePaymentRequiredHeader(value);
9
+ }
10
+ export function encodeResponse(value) {
11
+ return encodePaymentResponseHeader(value);
12
+ }
13
+ export function decodeSignature(value) {
14
+ return decodePaymentSignatureHeader(value);
15
+ }
16
+ export function matchRequirement(payload, requirement) {
17
+ const accepted = payload?.accepted ?? payload?.payment?.accepted ?? payload;
18
+ if (!accepted || typeof accepted !== "object")
19
+ return false;
20
+ return (accepted.scheme === requirement.scheme &&
21
+ accepted.network === requirement.network &&
22
+ String(accepted.amount) === requirement.amount &&
23
+ String(accepted.asset).toLowerCase() === requirement.asset.toLowerCase() &&
24
+ String(accepted.payTo) === requirement.payTo);
25
+ }
@@ -0,0 +1,53 @@
1
+ name: tron-nile-usdt
2
+ title: "TRON Nile USDT Provider"
3
+ description: "TRON Nile provider protected by x402 exact Permit2 USDT payment"
4
+ category: data
5
+ version: v1
6
+
7
+ forward_url: ${X402_PROVIDER_FORWARD_URL}
8
+ openapi_url: ${X402_PROVIDER_OPENAPI_URL}
9
+
10
+ display:
11
+ service_url: ${X402_GATEWAY_PUBLIC_BASE_URL}/providers/tron-nile-usdt
12
+ tags: ["tron-nile", "usdt", "x402"]
13
+
14
+ discovery:
15
+ use_case: "Use for validating TRON Nile USDT x402 payments."
16
+ spend_aware_usage:
17
+ - "Use small Nile USDT amounts while validating facilitator and gateway wiring."
18
+ when_to_use:
19
+ - "Use when testing x402 exact Permit2 payments on TRON Nile."
20
+ request_examples:
21
+ - "GET /providers/tron-nile-usdt/v1/ping"
22
+
23
+ routing:
24
+ type: proxy
25
+ auth:
26
+ method: header
27
+ key: Authorization
28
+ prefix: "Bearer "
29
+ value_from_env: X402_PROVIDER_API_TOKEN
30
+
31
+ operator:
32
+ network: tron-nile
33
+ currencies:
34
+ usd: ["USDT"]
35
+ recipient: ${X402_PROVIDER_RECIPIENT_TRON}
36
+ scheme: exact
37
+ protocol: exact
38
+ asset_transfer_method: permit2
39
+ facilitator_url: https://tn-facilitator.bankofai.io
40
+ facilitator_api_key_env: X402_FACILITATOR_API_KEY
41
+ valid_for_seconds: 300
42
+
43
+ endpoints:
44
+ - method: GET
45
+ path: /v1/ping
46
+ description: "TRON Nile USDT paid ping endpoint"
47
+ metering:
48
+ dimensions:
49
+ - direction: usage
50
+ unit: requests
51
+ scale: 1
52
+ tiers:
53
+ - price_usd: 0.0001
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@bankofai/x402-gateway",
3
+ "version": "1.0.0-beta.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "examples",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "bin": {
13
+ "x402-gateway": "./dist/cli.js"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "start": "node dist/cli.js",
18
+ "dev": "tsx src/cli.ts"
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "dependencies": {
24
+ "@bankofai/x402-core": "1.0.0",
25
+ "@bankofai/x402-evm": "1.0.0",
26
+ "@bankofai/x402-tron": "1.0.0",
27
+ "yaml": "^2.8.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^24.10.1",
31
+ "tsx": "^4.20.6",
32
+ "typescript": "^5.9.3"
33
+ }
34
+ }