@bankofai/x402-cli 1.0.0-beta.3 → 1.0.0-beta.4
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/dist/cli.js +15 -3
- package/dist/gateway/cli.js +33 -0
- package/dist/gateway/config.js +136 -0
- package/dist/gateway/server.js +249 -0
- package/dist/gateway/tokens.js +38 -0
- package/dist/gateway/x402.js +25 -0
- package/dist/tokens.js +2 -2
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -5,11 +5,13 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import YAML from "yaml";
|
|
10
11
|
import { createPaymentPayload, decodeRequired, decodeResponse, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
|
|
11
12
|
import { findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
|
|
12
13
|
const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "help", "human", "include-blocked", "json", "version"]);
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
13
15
|
function parseArgs(argv) {
|
|
14
16
|
const [command = "help", ...rest] = argv;
|
|
15
17
|
const positional = [];
|
|
@@ -569,7 +571,7 @@ function providerFilename(fqn) {
|
|
|
569
571
|
function defaultCatalogSource() {
|
|
570
572
|
return fs.existsSync(cachedCatalogPath())
|
|
571
573
|
? cachedCatalogPath()
|
|
572
|
-
: "https://
|
|
574
|
+
: "https://x402-catalog.bankofai.io/api/catalog.json";
|
|
573
575
|
}
|
|
574
576
|
function remoteBaseFromCatalogPayload(payload) {
|
|
575
577
|
const base = payload.base_url ?? payload.baseUrl;
|
|
@@ -696,7 +698,7 @@ function submissionCatalog(detail) {
|
|
|
696
698
|
description,
|
|
697
699
|
useCase,
|
|
698
700
|
i18n: detail.i18n ?? { "zh-CN": zhCopy(title, subtitle, description, useCase) },
|
|
699
|
-
logo: detail.logo ?? "https://
|
|
701
|
+
logo: detail.logo ?? "https://x402-catalog.bankofai.io/assets/providers/default.png",
|
|
700
702
|
category: detail.category ?? "other",
|
|
701
703
|
chains: detail.chains ?? [],
|
|
702
704
|
isFirstParty: Boolean(detail.is_first_party ?? detail.isFirstParty),
|
|
@@ -1067,10 +1069,20 @@ function executableInPath(name) {
|
|
|
1067
1069
|
}
|
|
1068
1070
|
return undefined;
|
|
1069
1071
|
}
|
|
1072
|
+
function resolveGatewayPackageRuntime() {
|
|
1073
|
+
try {
|
|
1074
|
+
return require.resolve("@bankofai/x402-gateway/dist/cli.js");
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
return undefined;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1070
1080
|
function gatewayCommand(options) {
|
|
1071
1081
|
const explicit = opt(options, "gateway-bin");
|
|
1082
|
+
const gatewayPackageRuntime = resolveGatewayPackageRuntime();
|
|
1072
1083
|
const candidates = [
|
|
1073
1084
|
explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
|
|
1085
|
+
gatewayPackageRuntime ? { file: gatewayPackageRuntime, source: "@bankofai/x402-gateway dependency" } : undefined,
|
|
1074
1086
|
{ file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
|
|
1075
1087
|
executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
|
|
1076
1088
|
{ file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
|
|
@@ -1087,7 +1099,7 @@ function gatewayCommand(options) {
|
|
|
1087
1099
|
// Try the next candidate.
|
|
1088
1100
|
}
|
|
1089
1101
|
}
|
|
1090
|
-
throw new Error("x402-gateway runtime not found. Install/
|
|
1102
|
+
throw new Error("x402-gateway runtime not found. Install @bankofai/x402-gateway, run from a checkout with ../x402-gateway/dist/cli.js, or pass --gateway-bin <path>.");
|
|
1091
1103
|
}
|
|
1092
1104
|
async function gatewayStart(args, options) {
|
|
1093
1105
|
const gateway = gatewayCommand(options);
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/tokens.js
CHANGED
|
@@ -56,9 +56,9 @@ export const TOKENS = {
|
|
|
56
56
|
},
|
|
57
57
|
"eip155:97": {
|
|
58
58
|
USDT: {
|
|
59
|
-
address: "
|
|
59
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
60
60
|
decimals: 18,
|
|
61
|
-
name: "USD
|
|
61
|
+
name: "Tether USD",
|
|
62
62
|
symbol: "USDT",
|
|
63
63
|
version: "1",
|
|
64
64
|
assetTransferMethod: "permit2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"bundle:gateway": "node scripts/bundle-gateway.mjs",
|
|
14
|
+
"prepack": "npm run build && npm run bundle:gateway",
|
|
13
15
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
14
16
|
"start": "node dist/cli.js",
|
|
15
17
|
"dev": "tsx src/cli.ts"
|