@bankofai/x402-gateway 1.0.0 → 1.0.1-beta.2
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 +11 -9
- package/dist/config.js +31 -12
- package/dist/server.js +40 -28
- package/dist/tokens.js +11 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
TypeScript reverse proxy for paid HTTP APIs. This version uses the npm
|
|
4
4
|
TypeScript x402 SDK packages only:
|
|
5
5
|
|
|
6
|
-
- `@bankofai/x402-core@1.0.
|
|
7
|
-
- `@bankofai/x402-evm@1.0.
|
|
8
|
-
- `@bankofai/x402-tron@1.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
|
-
Payment requirements
|
|
11
|
-
`extra.assetTransferMethod=permit2
|
|
10
|
+
Payment requirements support `scheme=exact` and TRON `scheme=exact_gasfree`.
|
|
11
|
+
Exact requirements add `extra.assetTransferMethod=permit2`; GasFree requirements
|
|
12
|
+
receive fee terms from the configured facilitator.
|
|
12
13
|
|
|
13
14
|
## Install
|
|
14
15
|
|
|
@@ -148,10 +149,11 @@ endpoints:
|
|
|
148
149
|
- price_usd: 0.002
|
|
149
150
|
```
|
|
150
151
|
|
|
151
|
-
`@bankofai/x402-*` 1.0.
|
|
152
|
-
`extra.assetTransferMethod: permit2
|
|
153
|
-
configs that say `exact_permit` are normalized
|
|
154
|
-
|
|
152
|
+
`@bankofai/x402-*` 1.0.1-beta.2 uses `scheme: exact` with
|
|
153
|
+
`extra.assetTransferMethod: permit2`, or TRON `scheme: exact_gasfree`. Older
|
|
154
|
+
provider configs that say `exact_permit` are normalized to `exact`. For GasFree,
|
|
155
|
+
set both `scheme` and `protocol` to `exact_gasfree`; the facilitator must support
|
|
156
|
+
GasFree for the selected TRON network and token.
|
|
155
157
|
|
|
156
158
|
Network aliases accepted:
|
|
157
159
|
|
package/dist/config.js
CHANGED
|
@@ -119,15 +119,31 @@ export function loadProvider(file) {
|
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
function normalizePaymentProtocol(config, file) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const rawSchemes = config.operator.schemes?.length
|
|
123
|
+
? config.operator.schemes
|
|
124
|
+
: [config.operator.protocol || config.operator.scheme || "exact"];
|
|
125
|
+
const schemes = [...new Set(rawSchemes.map(value => {
|
|
126
|
+
const raw = String(value).toLowerCase();
|
|
127
|
+
const normalized = raw.replace(/[-:\s]/g, "_");
|
|
128
|
+
if (!["exact", "exact_gasfree", "exact_permit", "permit2", "exact_permit2"].includes(normalized)) {
|
|
129
|
+
throw new Error(`${file}: unsupported x402 protocol ${raw}; use exact or exact_gasfree`);
|
|
130
|
+
}
|
|
131
|
+
return normalized === "exact_gasfree" ? "exact_gasfree" : "exact";
|
|
132
|
+
}))];
|
|
133
|
+
if (schemes.includes("exact_gasfree") && !config.operator.network.startsWith("tron:")) {
|
|
134
|
+
throw new Error(`${file}: exact_gasfree is supported only on TRON networks`);
|
|
135
|
+
}
|
|
136
|
+
config.operator.schemes = schemes;
|
|
137
|
+
config.operator.scheme = schemes[0];
|
|
138
|
+
config.operator.protocol = schemes[0];
|
|
139
|
+
if (schemes.includes("exact")) {
|
|
140
|
+
config.operator.asset_transfer_method = "permit2";
|
|
141
|
+
config.operator.assetTransferMethod = "permit2";
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
delete config.operator.asset_transfer_method;
|
|
145
|
+
delete config.operator.assetTransferMethod;
|
|
126
146
|
}
|
|
127
|
-
config.operator.scheme = "exact";
|
|
128
|
-
config.operator.protocol = "exact";
|
|
129
|
-
config.operator.asset_transfer_method = "permit2";
|
|
130
|
-
config.operator.assetTransferMethod = "permit2";
|
|
131
147
|
}
|
|
132
148
|
export function loadProviders(providerPath) {
|
|
133
149
|
const stat = fs.statSync(providerPath);
|
|
@@ -178,20 +194,23 @@ export function paymentRequirements(provider, price) {
|
|
|
178
194
|
const network = normalizeNetwork(provider.operator.network);
|
|
179
195
|
const symbols = provider.operator.currencies?.usd ?? ["USDT"];
|
|
180
196
|
const payTo = provider.recipients?.[provider.operator.recipient]?.account ?? provider.operator.recipient;
|
|
181
|
-
|
|
197
|
+
const schemes = provider.operator.schemes?.length
|
|
198
|
+
? provider.operator.schemes.map(scheme => scheme === "exact_gasfree" ? "exact_gasfree" : "exact")
|
|
199
|
+
: [provider.operator.scheme === "exact_gasfree" ? "exact_gasfree" : "exact"];
|
|
200
|
+
return schemes.flatMap(scheme => symbols.map(symbol => {
|
|
182
201
|
const token = getToken(network, symbol);
|
|
183
202
|
const transferMethod = provider.operator.assetTransferMethod || provider.operator.asset_transfer_method || token.assetTransferMethod;
|
|
184
203
|
const amount = toSmallestUnit(price, token.decimals);
|
|
185
204
|
if (amount === "0")
|
|
186
205
|
throw new Error(`positive price produced zero amount for ${symbol} on ${network}`);
|
|
187
206
|
return {
|
|
188
|
-
scheme
|
|
207
|
+
scheme,
|
|
189
208
|
network,
|
|
190
209
|
amount,
|
|
191
210
|
asset: token.address,
|
|
192
211
|
payTo,
|
|
193
212
|
maxTimeoutSeconds: provider.operator.valid_for_seconds ?? 300,
|
|
194
|
-
extra: transferMethod === "permit2" ? { assetTransferMethod: "permit2" } : {},
|
|
213
|
+
extra: scheme === "exact" && transferMethod === "permit2" ? { assetTransferMethod: "permit2" } : {},
|
|
195
214
|
};
|
|
196
|
-
});
|
|
215
|
+
}));
|
|
197
216
|
}
|
package/dist/server.js
CHANGED
|
@@ -5,10 +5,12 @@ import { decodeSignature, encodeRequired, encodeResponse, headers, matchRequirem
|
|
|
5
5
|
class HttpError extends Error {
|
|
6
6
|
status;
|
|
7
7
|
publicMessage;
|
|
8
|
-
|
|
8
|
+
responseHeaders;
|
|
9
|
+
constructor(status, publicMessage, message = publicMessage, responseHeaders = {}) {
|
|
9
10
|
super(message);
|
|
10
11
|
this.status = status;
|
|
11
12
|
this.publicMessage = publicMessage;
|
|
13
|
+
this.responseHeaders = responseHeaders;
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
class RequestTooLargeError extends HttpError {
|
|
@@ -52,7 +54,6 @@ const metrics = {
|
|
|
52
54
|
paidRequests: 0,
|
|
53
55
|
verifyFailures: 0,
|
|
54
56
|
settleFailures: 0,
|
|
55
|
-
feeQuoteFailures: 0,
|
|
56
57
|
upstreamFailures: 0,
|
|
57
58
|
};
|
|
58
59
|
async function readBody(request) {
|
|
@@ -89,7 +90,7 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
|
89
90
|
async function facilitatorPost(entry, path, body) {
|
|
90
91
|
const headers = { "content-type": "application/json" };
|
|
91
92
|
if (entry.facilitatorApiKey) {
|
|
92
|
-
headers
|
|
93
|
+
headers["x-api-key"] = entry.facilitatorApiKey;
|
|
93
94
|
}
|
|
94
95
|
const response = await fetchWithTimeout(new URL(path, entry.facilitatorUrl), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
|
|
95
96
|
const text = await response.text();
|
|
@@ -98,31 +99,43 @@ async function facilitatorPost(entry, path, body) {
|
|
|
98
99
|
data = text ? JSON.parse(text) : {};
|
|
99
100
|
}
|
|
100
101
|
catch {
|
|
102
|
+
logFacilitatorFailure(entry, path, response, body, { code: "invalid_json" });
|
|
101
103
|
throw new HttpError(502, "facilitator returned invalid response");
|
|
102
104
|
}
|
|
103
|
-
if (!response.ok)
|
|
104
|
-
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
logFacilitatorFailure(entry, path, response, body, data);
|
|
107
|
+
if (response.status === 429) {
|
|
108
|
+
const retryAfter = response.headers.get("retry-after");
|
|
109
|
+
throw new HttpError(429, "facilitator rate limited", `facilitator ${path} rate limited`, retryAfter ? { "retry-after": retryAfter } : {});
|
|
110
|
+
}
|
|
111
|
+
throw new HttpError(502, "facilitator request failed", `facilitator ${path} failed: ${response.status}`);
|
|
112
|
+
}
|
|
105
113
|
return data;
|
|
106
114
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
function resourceUrl(url) {
|
|
116
|
+
const path = `${url.pathname}${url.search}`;
|
|
117
|
+
const publicBaseUrl = process.env.X402_GATEWAY_PUBLIC_BASE_URL?.trim();
|
|
118
|
+
if (!publicBaseUrl)
|
|
119
|
+
return path;
|
|
120
|
+
return new URL(path, `${publicBaseUrl.replace(/\/+$/, "")}/`).toString();
|
|
121
|
+
}
|
|
122
|
+
function logFacilitatorFailure(entry, path, response, body, data) {
|
|
123
|
+
const requirement = body?.paymentRequirements;
|
|
124
|
+
const nestedError = data?.error && typeof data.error === "object" ? data.error : undefined;
|
|
125
|
+
const message = nestedError?.message ?? data?.message ?? data?.detail ??
|
|
126
|
+
(typeof data?.error === "string" ? data.error : undefined);
|
|
127
|
+
console.error(JSON.stringify({
|
|
128
|
+
event: "facilitator_request_failed",
|
|
129
|
+
provider: entry.config.name,
|
|
130
|
+
endpoint: path,
|
|
131
|
+
status: response.status,
|
|
132
|
+
scheme: requirement?.scheme,
|
|
133
|
+
network: requirement?.network,
|
|
134
|
+
errorCode: nestedError?.code ?? data?.code,
|
|
135
|
+
errorMessage: typeof message === "string" ? message.slice(0, 200) : undefined,
|
|
136
|
+
retryAfter: response.headers.get("retry-after") ?? undefined,
|
|
137
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
138
|
+
}));
|
|
126
139
|
}
|
|
127
140
|
function isVerifySuccess(verify) {
|
|
128
141
|
return verify?.valid === true || verify?.isValid === true;
|
|
@@ -265,7 +278,6 @@ export function createGatewayServer(providers) {
|
|
|
265
278
|
`x402_gateway_paid_requests_total ${metrics.paidRequests}`,
|
|
266
279
|
`x402_gateway_verify_failures_total ${metrics.verifyFailures}`,
|
|
267
280
|
`x402_gateway_settle_failures_total ${metrics.settleFailures}`,
|
|
268
|
-
`x402_gateway_fee_quote_failures_total ${metrics.feeQuoteFailures}`,
|
|
269
281
|
`x402_gateway_upstream_failures_total ${metrics.upstreamFailures}`,
|
|
270
282
|
"",
|
|
271
283
|
].join("\n"));
|
|
@@ -295,11 +307,11 @@ export function createGatewayServer(providers) {
|
|
|
295
307
|
}
|
|
296
308
|
const paymentHeader = request.headers[headers.signature.toLowerCase()];
|
|
297
309
|
if (!paymentHeader || Array.isArray(paymentHeader)) {
|
|
298
|
-
const accepts =
|
|
310
|
+
const accepts = requirements;
|
|
299
311
|
const challenge = {
|
|
300
312
|
x402Version: 2,
|
|
301
313
|
error: "Payment required",
|
|
302
|
-
resource: { url: url
|
|
314
|
+
resource: { url: resourceUrl(url) },
|
|
303
315
|
accepts,
|
|
304
316
|
};
|
|
305
317
|
json(response, 402, challenge, { [headers.required]: encodeRequired(challenge) });
|
|
@@ -348,7 +360,7 @@ export function createGatewayServer(providers) {
|
|
|
348
360
|
}
|
|
349
361
|
catch (error) {
|
|
350
362
|
if (error instanceof HttpError) {
|
|
351
|
-
json(response, error.status, { error: error.publicMessage });
|
|
363
|
+
json(response, error.status, { error: error.publicMessage }, error.responseHeaders);
|
|
352
364
|
return;
|
|
353
365
|
}
|
|
354
366
|
console.error(error);
|
package/dist/tokens.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const TOKENS = {
|
|
2
|
-
"tron:
|
|
2
|
+
"tron:0x2b6653dc": {
|
|
3
3
|
USDT: { address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", decimals: 6, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
|
|
4
4
|
USDD: { address: "TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz", decimals: 18, name: "Decentralized USD", symbol: "USDD", assetTransferMethod: "permit2" },
|
|
5
5
|
},
|
|
6
|
-
"tron:
|
|
6
|
+
"tron:0xcd8690dc": {
|
|
7
7
|
USDT: { address: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", decimals: 6, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
|
|
8
8
|
USDD: { address: "TGjgvdTWWrybVLaVeFqSyVqJQWjxqRYbaK", decimals: 18, name: "Decentralized USD", symbol: "USDD", assetTransferMethod: "permit2" },
|
|
9
9
|
},
|
|
@@ -17,9 +17,15 @@ export const TOKENS = {
|
|
|
17
17
|
};
|
|
18
18
|
export function normalizeNetwork(network) {
|
|
19
19
|
return ({
|
|
20
|
-
"tron-mainnet": "tron:
|
|
21
|
-
"tron
|
|
22
|
-
"
|
|
20
|
+
"tron-mainnet": "tron:0x2b6653dc",
|
|
21
|
+
"tron:mainnet": "tron:0x2b6653dc",
|
|
22
|
+
"mainnet": "tron:0x2b6653dc",
|
|
23
|
+
"tron-shasta": "tron:0x94a9059e",
|
|
24
|
+
"tron:shasta": "tron:0x94a9059e",
|
|
25
|
+
"shasta": "tron:0x94a9059e",
|
|
26
|
+
"tron-nile": "tron:0xcd8690dc",
|
|
27
|
+
"tron:nile": "tron:0xcd8690dc",
|
|
28
|
+
"nile": "tron:0xcd8690dc",
|
|
23
29
|
"bsc-mainnet": "eip155:56",
|
|
24
30
|
"bsc-testnet": "eip155:97",
|
|
25
31
|
}[network] ?? network);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-gateway",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1-beta.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@bankofai/x402-core": "1.0.
|
|
26
|
-
"@bankofai/x402-evm": "1.0.
|
|
27
|
-
"@bankofai/x402-tron": "1.0.
|
|
25
|
+
"@bankofai/x402-core": "1.0.1-beta.4",
|
|
26
|
+
"@bankofai/x402-evm": "1.0.1-beta.4",
|
|
27
|
+
"@bankofai/x402-tron": "1.0.1-beta.4",
|
|
28
28
|
"yaml": "^2.8.2"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|