@elizaos/plugin-x402 2.0.0-alpha.6 → 2.0.3-beta.5

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/dist/index.d.ts +57 -2
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +2542 -1915
  6. package/dist/index.js.map +14 -21
  7. package/dist/payment-config.d.ts +256 -0
  8. package/dist/payment-config.d.ts.map +1 -0
  9. package/dist/payment-wrapper.d.ts +42 -0
  10. package/dist/payment-wrapper.d.ts.map +1 -0
  11. package/dist/startup-validator.d.ts +28 -0
  12. package/dist/startup-validator.d.ts.map +1 -0
  13. package/dist/types.d.ts +158 -0
  14. package/dist/types.d.ts.map +1 -0
  15. package/dist/x402-facilitator-binding.d.ts +9 -0
  16. package/dist/x402-facilitator-binding.d.ts.map +1 -0
  17. package/dist/x402-replay-durable.d.ts +30 -0
  18. package/dist/x402-replay-durable.d.ts.map +1 -0
  19. package/dist/x402-replay-guard.d.ts +28 -0
  20. package/dist/x402-replay-guard.d.ts.map +1 -0
  21. package/dist/x402-replay-keys.d.ts +21 -0
  22. package/dist/x402-replay-keys.d.ts.map +1 -0
  23. package/dist/x402-resolve.d.ts +6 -0
  24. package/dist/x402-resolve.d.ts.map +1 -0
  25. package/dist/x402-standard-payment.d.ts +130 -0
  26. package/dist/x402-standard-payment.d.ts.map +1 -0
  27. package/dist/x402-types.d.ts +130 -0
  28. package/dist/x402-types.d.ts.map +1 -0
  29. package/package.json +63 -94
  30. package/src/__tests__/core-test-mock.ts +10 -0
  31. package/src/index.ts +115 -0
  32. package/src/payment-config.ts +737 -0
  33. package/src/payment-wrapper.test.ts +234 -0
  34. package/src/payment-wrapper.ts +1997 -0
  35. package/src/startup-validator.test.ts +86 -0
  36. package/src/startup-validator.ts +351 -0
  37. package/src/types.ts +177 -0
  38. package/src/x402-facilitator-binding.ts +104 -0
  39. package/src/x402-replay-durable.ts +320 -0
  40. package/src/x402-replay-guard.ts +165 -0
  41. package/src/x402-replay-keys.ts +151 -0
  42. package/src/x402-resolve.ts +43 -0
  43. package/src/x402-standard-payment.ts +519 -0
  44. package/src/x402-types.ts +376 -0
package/dist/index.js CHANGED
@@ -1,2070 +1,2697 @@
1
- // actions/check-payment-history.ts
2
- import { logger } from "@elizaos/core";
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
- // utils.ts
5
- var ONE_DAY_MS = 24 * 60 * 60 * 1000;
6
- function formatUsd(baseUnits) {
7
- const dollars = Number(baseUnits) / 1e6;
8
- return `$${dollars.toFixed(2)}`;
4
+ // src/payment-config.ts
5
+ import { logger } from "@elizaos/core";
6
+ var BUILT_IN_NETWORKS = ["BASE", "SOLANA", "POLYGON", "BSC"];
7
+ var DEFAULT_NETWORK = "SOLANA";
8
+ function toX402Network(network) {
9
+ const networkMap = {
10
+ BASE: "base",
11
+ SOLANA: "solana",
12
+ POLYGON: "polygon",
13
+ BSC: "bsc"
14
+ };
15
+ const mappedNetwork = networkMap[network];
16
+ if (!mappedNetwork) {
17
+ throw new Error(`Network '${network}' is not supported by x402scan. ` + `Supported networks: ${BUILT_IN_NETWORKS.join(", ")}`);
18
+ }
19
+ return mappedNetwork;
9
20
  }
10
- function truncateAddress(address) {
11
- if (address.length <= 10)
12
- return address;
13
- return `${address.slice(0, 6)}...${address.slice(-4)}`;
21
+ var BUNDLED_EXAMPLE_EVM_PAYOUT = "0x066E94e1200aa765d0A6392777D543Aa6Dea606C";
22
+ var BUNDLED_EXAMPLE_SOLANA_PAYOUT = "3nMBmufBUBVnk28sTp3NsrSJsdVGTyLZYmsqpMFaUT9J";
23
+ function paymentAddressIsBundledExample(network, paymentAddress) {
24
+ const a = paymentAddress.trim();
25
+ if (!a)
26
+ return false;
27
+ if (network === "SOLANA")
28
+ return a === BUNDLED_EXAMPLE_SOLANA_PAYOUT;
29
+ if (network === "BASE" || network === "POLYGON" || network === "BSC") {
30
+ return a.toLowerCase() === BUNDLED_EXAMPLE_EVM_PAYOUT.toLowerCase();
31
+ }
32
+ return false;
14
33
  }
15
- function usdToBaseUnits(usd) {
16
- return BigInt(Math.round(usd * 1e6));
34
+ var PAYMENT_ADDRESSES = {
35
+ BASE: process.env.BASE_PUBLIC_KEY || process.env.PAYMENT_WALLET_BASE || BUNDLED_EXAMPLE_EVM_PAYOUT,
36
+ SOLANA: process.env.SOLANA_PUBLIC_KEY || process.env.PAYMENT_WALLET_SOLANA || BUNDLED_EXAMPLE_SOLANA_PAYOUT,
37
+ POLYGON: process.env.POLYGON_PUBLIC_KEY || process.env.PAYMENT_WALLET_POLYGON || "",
38
+ BSC: process.env.BSC_PUBLIC_KEY || process.env.PAYMENT_WALLET_BSC || BUNDLED_EXAMPLE_EVM_PAYOUT
39
+ };
40
+ function getBaseUrl() {
41
+ if (process.env.X402_BASE_URL) {
42
+ return process.env.X402_BASE_URL.replace(/\/$/, "");
43
+ }
44
+ return "https://x402.elizacloud.ai";
17
45
  }
18
-
19
- // actions/check-payment-history.ts
20
- var checkPaymentHistoryAction = {
21
- name: "CHECK_PAYMENT_HISTORY",
22
- description: "Check x402 payment history including spending summary and recent transactions. Use when asked about payment activity, spending, or earnings.",
23
- similes: [
24
- "check payments",
25
- "payment history",
26
- "spending summary",
27
- "how much have I spent",
28
- "payment transactions",
29
- "show payments"
30
- ],
31
- parameters: [
32
- {
33
- name: "limit",
34
- description: "Maximum number of recent transactions to show (default: 10)",
35
- required: false,
36
- schema: { type: "number" }
37
- }
38
- ],
39
- validate: async (runtime, message, state, options) => {
40
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
41
- const __avText = __avTextRaw.toLowerCase();
42
- const __avKeywords = ["check", "payment", "history"];
43
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
44
- const __avRegex = new RegExp("\\b(?:check|payment|history)\\b", "i");
45
- const __avRegexOk = __avRegex.test(__avText);
46
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
47
- const __avExpectedSource = "";
48
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
49
- const __avOptions = options && typeof options === "object" ? options : {};
50
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
51
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
52
- return false;
53
- }
54
- const __avLegacyValidate = async (runtime2) => {
55
- const service = runtime2.getService("x402_payment");
56
- return !!service && service.isActive();
57
- };
58
- try {
59
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
60
- } catch {
61
- return false;
62
- }
46
+ function toResourceUrl(path) {
47
+ const baseUrl = getBaseUrl();
48
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
49
+ return `${baseUrl}${cleanPath}`;
50
+ }
51
+ var SOLANA_TOKENS = {
52
+ USDC: {
53
+ symbol: "USDC",
54
+ address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
55
+ decimals: 6
63
56
  },
64
- handler: async (runtime, _message, _state, options, callback) => {
65
- const service = runtime.getService("x402_payment");
66
- if (!service || !service.isActive()) {
67
- logger.warn("[x402] CHECK_PAYMENT_HISTORY: Service not available or inactive");
68
- if (callback) {
69
- await callback({
70
- text: "Payment tracking is not active. The x402 payment service is not configured.",
71
- actions: []
72
- });
73
- }
74
- return { success: false, error: "x402 service not available" };
75
- }
76
- const params = options?.parameters;
77
- const limit = typeof params?.limit === "number" ? params.limit : 10;
78
- try {
79
- const summary = await service.getSummary(ONE_DAY_MS);
80
- const recentTxns = await service.getRecentTransactions(limit);
81
- const lines = [];
82
- const walletAddress = service.getWalletAddress();
83
- const network = service.getNetwork();
84
- lines.push(`**Payment Summary** (${network})`);
85
- lines.push(`Wallet: ${walletAddress ? truncateAddress(walletAddress) : "N/A"}`);
86
- lines.push("");
87
- lines.push("**Last 24 Hours:**");
88
- lines.push(`- Spent: ${formatUsd(summary.totalSpent)} (${summary.outgoingCount} transactions)`);
89
- lines.push(`- Earned: ${formatUsd(summary.totalEarned)} (${summary.incomingCount} transactions)`);
90
- const net = summary.totalEarned - summary.totalSpent;
91
- const netDisplay = net < 0n ? `-${formatUsd(-net)}` : `+${formatUsd(net)}`;
92
- lines.push(`- Net: ${netDisplay}`);
93
- lines.push("");
94
- lines.push(`Circuit Breaker: ${service.getCircuitBreakerState()}`);
95
- lines.push("");
96
- if (recentTxns.length > 0) {
97
- lines.push(`**Recent Transactions** (last ${recentTxns.length}):`);
98
- for (const txn of recentTxns) {
99
- const direction = txn.direction === "outgoing" ? "SENT" : "RECV";
100
- const counterpartyDisplay = truncateAddress(txn.counterparty);
101
- const time = new Date(txn.createdAt).toLocaleString();
102
- lines.push(`- [${direction}] ${formatUsd(txn.amount)} ${txn.direction === "outgoing" ? "to" : "from"} ${counterpartyDisplay} — ${txn.resource || "N/A"} (${time}) [${txn.status}]`);
103
- }
104
- } else {
105
- lines.push("No recent transactions.");
106
- }
107
- const responseText = lines.join(`
108
- `);
109
- if (callback) {
110
- await callback({
111
- text: responseText,
112
- actions: []
113
- });
114
- }
115
- return {
116
- success: true,
117
- text: responseText,
118
- data: {
119
- totalSpent: formatUsd(summary.totalSpent),
120
- totalEarned: formatUsd(summary.totalEarned),
121
- outgoingCount: summary.outgoingCount,
122
- incomingCount: summary.incomingCount,
123
- recentTransactionCount: recentTxns.length
124
- }
125
- };
126
- } catch (err) {
127
- const errorMessage = err instanceof Error ? err.message : String(err);
128
- logger.error(`[x402] CHECK_PAYMENT_HISTORY: Failed: ${errorMessage}`);
129
- if (callback) {
130
- await callback({
131
- text: `Failed to retrieve payment history: ${errorMessage}`,
132
- actions: []
133
- });
134
- }
135
- return { success: false, error: errorMessage };
136
- }
57
+ AI16Z: {
58
+ symbol: "ai16z",
59
+ address: "HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC",
60
+ decimals: 6
137
61
  },
138
- examples: [
139
- [
140
- {
141
- name: "user",
142
- content: {
143
- text: "How much have you spent today?"
144
- }
145
- },
146
- {
147
- name: "assistant",
148
- content: {
149
- text: "Let me check my payment history for today.",
150
- actions: ["CHECK_PAYMENT_HISTORY"]
151
- }
152
- }
153
- ],
154
- [
155
- {
156
- name: "user",
157
- content: {
158
- text: "Show me your recent payment transactions"
159
- }
160
- },
161
- {
162
- name: "assistant",
163
- content: {
164
- text: "Here are my recent x402 payment transactions.",
165
- actions: ["CHECK_PAYMENT_HISTORY"]
166
- }
62
+ DEGENAI: {
63
+ symbol: "degenai",
64
+ address: "Gu3LDkn7Vx3bmCzLafYNKcDxv2mH7YN44NJZFXnypump",
65
+ decimals: 6
66
+ },
67
+ ELIZAOS: {
68
+ symbol: "elizaOS",
69
+ address: "DuMbhu7mvQvqQHGcnikDgb4XegXJRyhUBfdU22uELiZA",
70
+ decimals: 6
71
+ }
72
+ };
73
+ var BASE_TOKENS = {
74
+ USDC: {
75
+ symbol: "USDC",
76
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
77
+ decimals: 6
78
+ },
79
+ ELIZAOS: {
80
+ symbol: "elizaOS",
81
+ address: "0xea17Df5Cf6D172224892B5477A16ACb111182478",
82
+ decimals: 18
83
+ }
84
+ };
85
+ var POLYGON_TOKENS = {
86
+ USDC: {
87
+ symbol: "USDC",
88
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
89
+ decimals: 6
90
+ }
91
+ };
92
+ var BSC_TOKENS = {
93
+ USDC: {
94
+ symbol: "USDC",
95
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
96
+ decimals: 18
97
+ }
98
+ };
99
+ var PAYMENT_RECEIVER_ADDRESS = PAYMENT_ADDRESSES[DEFAULT_NETWORK] || "";
100
+ var PAYMENT_CONFIGS = {
101
+ base_usdc: {
102
+ network: "BASE",
103
+ assetNamespace: "erc20",
104
+ assetReference: BASE_TOKENS.USDC.address,
105
+ paymentAddress: PAYMENT_ADDRESSES.BASE ?? BUNDLED_EXAMPLE_EVM_PAYOUT,
106
+ symbol: "USDC",
107
+ chainId: "8453"
108
+ },
109
+ solana_usdc: {
110
+ network: "SOLANA",
111
+ assetNamespace: "spl-token",
112
+ assetReference: SOLANA_TOKENS.USDC.address,
113
+ paymentAddress: PAYMENT_ADDRESSES.SOLANA ?? BUNDLED_EXAMPLE_SOLANA_PAYOUT,
114
+ symbol: "USDC"
115
+ },
116
+ polygon_usdc: {
117
+ network: "POLYGON",
118
+ assetNamespace: "erc20",
119
+ assetReference: POLYGON_TOKENS.USDC.address,
120
+ paymentAddress: PAYMENT_ADDRESSES.POLYGON || "",
121
+ symbol: "USDC",
122
+ chainId: "137"
123
+ },
124
+ bsc_usdc: {
125
+ network: "BSC",
126
+ assetNamespace: "erc20",
127
+ assetReference: BSC_TOKENS.USDC.address,
128
+ paymentAddress: PAYMENT_ADDRESSES.BSC ?? BUNDLED_EXAMPLE_EVM_PAYOUT,
129
+ symbol: "USDC",
130
+ chainId: "56"
131
+ },
132
+ base_elizaos: {
133
+ network: "BASE",
134
+ assetNamespace: "erc20",
135
+ assetReference: BASE_TOKENS.ELIZAOS.address,
136
+ paymentAddress: PAYMENT_ADDRESSES.BASE ?? BUNDLED_EXAMPLE_EVM_PAYOUT,
137
+ symbol: "elizaOS",
138
+ chainId: "8453"
139
+ },
140
+ solana_elizaos: {
141
+ network: "SOLANA",
142
+ assetNamespace: "spl-token",
143
+ assetReference: SOLANA_TOKENS.ELIZAOS.address,
144
+ paymentAddress: PAYMENT_ADDRESSES.SOLANA ?? BUNDLED_EXAMPLE_SOLANA_PAYOUT,
145
+ symbol: "elizaOS"
146
+ },
147
+ solana_degenai: {
148
+ network: "SOLANA",
149
+ assetNamespace: "spl-token",
150
+ assetReference: SOLANA_TOKENS.DEGENAI.address,
151
+ paymentAddress: PAYMENT_ADDRESSES.SOLANA ?? BUNDLED_EXAMPLE_SOLANA_PAYOUT,
152
+ symbol: "degenai"
153
+ }
154
+ };
155
+ function getCAIP19FromConfig(config) {
156
+ const chainNamespace = config.network === "SOLANA" ? "solana" : "eip155";
157
+ const chainReference = config.chainId || (config.network === "BASE" ? "8453" : config.network === "POLYGON" ? "137" : config.network === "BSC" ? "56" : "1");
158
+ const chainId = `${chainNamespace}:${chainReference}`;
159
+ const assetId = `${config.assetNamespace}:${config.assetReference}`;
160
+ return `${chainId}/${assetId}`;
161
+ }
162
+ var CUSTOM_PAYMENT_CONFIGS = {};
163
+ function registerX402Config(name, config, options) {
164
+ if (PAYMENT_CONFIGS[name] && !options?.override) {
165
+ throw new Error(`Payment config '${name}' already exists. Use override: true to replace it.`);
166
+ }
167
+ const registryKey = options?.agentId ? `${options.agentId}:${name}` : name;
168
+ if (CUSTOM_PAYMENT_CONFIGS[registryKey] && !options?.override) {
169
+ throw new Error(`Payment config '${registryKey}' is already registered. Use override: true to replace it.`);
170
+ }
171
+ CUSTOM_PAYMENT_CONFIGS[registryKey] = config;
172
+ logger.debug({ registryKey, symbol: config.symbol, network: config.network }, "[x402] registered payment config");
173
+ }
174
+ function getPaymentConfig(name, agentId) {
175
+ if (agentId) {
176
+ const agentConfig = CUSTOM_PAYMENT_CONFIGS[`${agentId}:${name}`];
177
+ if (agentConfig)
178
+ return agentConfig;
179
+ }
180
+ const customConfig = CUSTOM_PAYMENT_CONFIGS[name];
181
+ if (customConfig)
182
+ return customConfig;
183
+ const builtInConfig = PAYMENT_CONFIGS[name];
184
+ if (!builtInConfig) {
185
+ const available = [
186
+ ...Object.keys(PAYMENT_CONFIGS),
187
+ ...Object.keys(CUSTOM_PAYMENT_CONFIGS).filter((k) => !k.includes(":"))
188
+ ];
189
+ throw new Error(`Unknown payment config '${name}'. Available: ${available.join(", ")}`);
190
+ }
191
+ return builtInConfig;
192
+ }
193
+ function listX402Configs(agentId) {
194
+ const configs = new Set([
195
+ ...Object.keys(PAYMENT_CONFIGS),
196
+ ...Object.keys(CUSTOM_PAYMENT_CONFIGS).filter((k) => !k.includes(":"))
197
+ ]);
198
+ if (agentId) {
199
+ for (const k of Object.keys(CUSTOM_PAYMENT_CONFIGS)) {
200
+ if (k.startsWith(`${agentId}:`)) {
201
+ const short = k.split(":")[1];
202
+ if (short)
203
+ configs.add(short);
167
204
  }
168
- ]
169
- ]
205
+ }
206
+ }
207
+ return Array.from(configs).sort();
208
+ }
209
+ function getPaymentAddress(network) {
210
+ const address = PAYMENT_ADDRESSES[network];
211
+ if (!address) {
212
+ throw new Error(`No payment address configured for network '${network}'. ` + `Supported networks: ${BUILT_IN_NETWORKS.join(", ")}. ` + `Set ${network}_PUBLIC_KEY in your environment.`);
213
+ }
214
+ return address;
215
+ }
216
+ var TOKEN_PRICES_USD = {
217
+ USDC: 1,
218
+ ai16z: Number.parseFloat(process.env.AI16Z_PRICE_USD || "0.5"),
219
+ degenai: Number.parseFloat(process.env.DEGENAI_PRICE_USD || "0.01"),
220
+ elizaOS: Number.parseFloat(process.env.ELIZAOS_PRICE_USD || "0.05"),
221
+ ETH: 2000
170
222
  };
223
+ function usdDecimalStringToRational(raw) {
224
+ const s = raw.replace(/^\$/, "").trim();
225
+ if (!/^\d+(\.\d+)?$/.test(s)) {
226
+ throw new Error(`Invalid USD decimal: ${raw}`);
227
+ }
228
+ const [wi, fr = ""] = s.split(".");
229
+ const den = 10n ** BigInt(fr.length);
230
+ const whole = BigInt(wi || "0");
231
+ const frac = fr ? BigInt(fr) : 0n;
232
+ const num = whole * den + frac;
233
+ if (num <= 0n) {
234
+ throw new Error(`USD amount must be positive: ${raw}`);
235
+ }
236
+ return { num, den };
237
+ }
238
+ function envUsdPerTokenRational(envKey, fallback) {
239
+ const v = process.env[envKey]?.trim();
240
+ return usdDecimalStringToRational(v && v.length > 0 ? v : fallback);
241
+ }
242
+ function getTokenUsdPerTokenRational(asset, _network) {
243
+ const upper = asset.toUpperCase();
244
+ if (upper === "USDC")
245
+ return { num: 1n, den: 1n };
246
+ if (asset === "elizaOS" || upper === "ELIZAOS") {
247
+ return envUsdPerTokenRational("ELIZAOS_PRICE_USD", "0.05");
248
+ }
249
+ if (upper === "DEGENAI" || asset === "degenai") {
250
+ return envUsdPerTokenRational("DEGENAI_PRICE_USD", "0.01");
251
+ }
252
+ if (upper === "AI16Z" || asset === "ai16z") {
253
+ return envUsdPerTokenRational("AI16Z_PRICE_USD", "0.5");
254
+ }
255
+ if (upper === "ETH")
256
+ return { num: 2000n, den: 1n };
257
+ return { num: 1n, den: 1n };
258
+ }
259
+ function getTokenDecimals(asset, network) {
260
+ if (network === "SOLANA") {
261
+ const solanaToken2 = Object.values(SOLANA_TOKENS).find((t) => t.symbol === asset);
262
+ if (solanaToken2)
263
+ return solanaToken2.decimals;
264
+ }
265
+ if (network === "BASE") {
266
+ const baseToken2 = Object.values(BASE_TOKENS).find((t) => t.symbol === asset);
267
+ if (baseToken2)
268
+ return baseToken2.decimals;
269
+ }
270
+ if (network === "POLYGON") {
271
+ const polygonToken2 = Object.values(POLYGON_TOKENS).find((t) => t.symbol === asset);
272
+ if (polygonToken2)
273
+ return polygonToken2.decimals;
274
+ }
275
+ if (network === "BSC") {
276
+ const bscToken2 = Object.values(BSC_TOKENS).find((t) => t.symbol === asset);
277
+ if (bscToken2)
278
+ return bscToken2.decimals;
279
+ }
280
+ const solanaToken = Object.values(SOLANA_TOKENS).find((t) => t.symbol === asset);
281
+ if (solanaToken)
282
+ return solanaToken.decimals;
283
+ const baseToken = Object.values(BASE_TOKENS).find((t) => t.symbol === asset);
284
+ if (baseToken)
285
+ return baseToken.decimals;
286
+ const polygonToken = Object.values(POLYGON_TOKENS).find((t) => t.symbol === asset);
287
+ if (polygonToken)
288
+ return polygonToken.decimals;
289
+ const bscToken = Object.values(BSC_TOKENS).find((t) => t.symbol === asset);
290
+ if (bscToken)
291
+ return bscToken.decimals;
292
+ if (asset === "USDC")
293
+ return 6;
294
+ if (asset === "ETH")
295
+ return 18;
296
+ return 6;
297
+ }
298
+ function atomicAmountForPriceInCents(priceInCents, config) {
299
+ if (!Number.isFinite(priceInCents) || priceInCents <= 0) {
300
+ throw new Error("priceInCents must be a positive finite number");
301
+ }
302
+ const cents = BigInt(Math.floor(priceInCents));
303
+ const { num: p, den: q } = getTokenUsdPerTokenRational(config.symbol, config.network);
304
+ const dec = getTokenDecimals(config.symbol, config.network);
305
+ if (dec < 0 || dec > 120) {
306
+ throw new Error("invalid token decimals for payment config");
307
+ }
308
+ const scale = 10n ** BigInt(dec);
309
+ const numer = cents * q * scale;
310
+ const denom = 100n * p;
311
+ if (denom === 0n) {
312
+ throw new Error("invalid token USD price (zero denominator)");
313
+ }
314
+ return ((numer + denom - 1n) / denom).toString();
315
+ }
316
+ function getX402Health() {
317
+ const networks = ["BASE", "SOLANA", "POLYGON", "BSC"];
318
+ return {
319
+ networks: networks.map((network) => ({
320
+ network,
321
+ configured: !!PAYMENT_ADDRESSES[network] && PAYMENT_ADDRESSES[network] !== "",
322
+ address: PAYMENT_ADDRESSES[network] || null
323
+ })),
324
+ facilitator: {
325
+ url: process.env.X402_FACILITATOR_URL || null,
326
+ configured: !!process.env.X402_FACILITATOR_URL
327
+ }
328
+ };
329
+ }
330
+ // src/payment-wrapper.ts
331
+ import { logger as logger5 } from "@elizaos/core";
332
+ import {
333
+ recoverTypedDataAddress
334
+ } from "viem";
335
+ import { base, bsc, mainnet, polygon } from "viem/chains";
171
336
 
172
- // actions/set-payment-policy.ts
337
+ // src/startup-validator.ts
173
338
  import { logger as logger2 } from "@elizaos/core";
174
- var setPaymentPolicyAction = {
175
- name: "SET_PAYMENT_POLICY",
176
- description: "Manage payment policies for the x402 payment service. Set per-transaction limits, daily spending limits, or block/allow specific recipient addresses.",
177
- similes: [
178
- "set payment policy",
179
- "update payment limits",
180
- "change spending limit",
181
- "block recipient",
182
- "allow recipient",
183
- "set max payment",
184
- "set daily limit",
185
- "payment policy"
186
- ],
187
- parameters: [
188
- {
189
- name: "maxPerPaymentUsd",
190
- description: "Maximum amount in USD for a single outgoing payment (e.g. 5.0 for $5.00)",
191
- required: false,
192
- schema: { type: "number" }
193
- },
194
- {
195
- name: "maxDailyUsd",
196
- description: "Maximum total USD spend per day (e.g. 50.0 for $50.00)",
197
- required: false,
198
- schema: { type: "number" }
199
- },
200
- {
201
- name: "blockRecipient",
202
- description: "Ethereum address to add to the blocklist (payments to this address will be rejected)",
203
- required: false,
204
- schema: { type: "string" }
205
- },
206
- {
207
- name: "allowRecipient",
208
- description: "Ethereum address to add to the allowlist (when allowlist is non-empty, only listed addresses can receive payments)",
209
- required: false,
210
- schema: { type: "string" }
211
- }
212
- ],
213
- validate: async (runtime, message, state, options) => {
214
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
215
- const __avText = __avTextRaw.toLowerCase();
216
- const __avKeywords = ["set", "payment", "policy"];
217
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
218
- const __avRegex = new RegExp("\\b(?:set|payment|policy)\\b", "i");
219
- const __avRegexOk = __avRegex.test(__avText);
220
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
221
- const __avExpectedSource = "";
222
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
223
- const __avOptions = options && typeof options === "object" ? options : {};
224
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
225
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
226
- return false;
339
+ function validatePaymentConfig(configName, agentId) {
340
+ const errors = [];
341
+ const warnings = [];
342
+ try {
343
+ const config = getPaymentConfig(configName, agentId);
344
+ if (!config.network) {
345
+ errors.push(`Config '${configName}': missing 'network'`);
227
346
  }
228
- const __avLegacyValidate = async (runtime2) => {
229
- const service = runtime2.getService("x402_payment");
230
- return !!service && service.isActive();
231
- };
232
- try {
233
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
234
- } catch {
235
- return false;
347
+ if (!config.assetNamespace) {
348
+ errors.push(`Config '${configName}': missing 'assetNamespace'`);
236
349
  }
237
- },
238
- handler: async (runtime, _message, _state, options, callback) => {
239
- const service = runtime.getService("x402_payment");
240
- if (!service || !service.isActive()) {
241
- logger2.warn("[x402] SET_PAYMENT_POLICY: Service not available or inactive");
242
- if (callback) {
243
- await callback({
244
- text: "I'm unable to update payment policies right now. The x402 payment service is not configured or is inactive.",
245
- actions: []
246
- });
247
- }
248
- return { success: false, error: "x402 service not available" };
249
- }
250
- const rawParams = options?.parameters;
251
- const maxPerPaymentUsd = rawParams?.maxPerPaymentUsd;
252
- const maxDailyUsd = rawParams?.maxDailyUsd;
253
- const blockRecipient = rawParams?.blockRecipient;
254
- const allowRecipient = rawParams?.allowRecipient;
255
- if (maxPerPaymentUsd === undefined && maxDailyUsd === undefined && !blockRecipient && !allowRecipient) {
256
- if (callback) {
257
- await callback({
258
- text: "Please specify at least one policy change: maxPerPaymentUsd, maxDailyUsd, blockRecipient, or allowRecipient.",
259
- actions: []
260
- });
261
- }
262
- return { success: false, error: "No policy parameters provided" };
350
+ if (!config.assetReference) {
351
+ errors.push(`Config '${configName}': missing 'assetReference'`);
263
352
  }
264
- const changes = [];
265
- try {
266
- if (maxPerPaymentUsd !== undefined) {
267
- if (maxPerPaymentUsd <= 0) {
268
- if (callback) {
269
- await callback({
270
- text: "maxPerPaymentUsd must be a positive number.",
271
- actions: []
272
- });
273
- }
274
- return { success: false, error: "Invalid maxPerPaymentUsd" };
353
+ if (!config.paymentAddress) {
354
+ errors.push(`Config '${configName}': missing 'paymentAddress' (wallet address required)`);
355
+ }
356
+ if (!config.symbol) {
357
+ errors.push(`Config '${configName}': missing 'symbol'`);
358
+ }
359
+ if (config.paymentAddress) {
360
+ if (config.network === "SOLANA") {
361
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(config.paymentAddress)) {
362
+ errors.push(`Config '${configName}': invalid Solana address format`);
275
363
  }
276
- service.updatePolicy({
277
- outgoing: {
278
- maxPerTransaction: usdToBaseUnits(maxPerPaymentUsd)
279
- }
280
- });
281
- changes.push(`Max per-payment limit set to $${maxPerPaymentUsd.toFixed(2)}`);
282
- logger2.info(`[x402] SET_PAYMENT_POLICY: maxPerTransaction set to $${maxPerPaymentUsd}`);
283
- }
284
- if (maxDailyUsd !== undefined) {
285
- if (maxDailyUsd <= 0) {
286
- if (callback) {
287
- await callback({
288
- text: "maxDailyUsd must be a positive number.",
289
- actions: []
290
- });
291
- }
292
- return { success: false, error: "Invalid maxDailyUsd" };
364
+ } else if (config.network === "BASE" || config.network === "POLYGON" || config.assetNamespace === "erc20") {
365
+ if (!/^0x[a-fA-F0-9]{40}$/.test(config.paymentAddress)) {
366
+ errors.push(`Config '${configName}': invalid EVM address format (should be 0x...)`);
293
367
  }
294
- service.updatePolicy({
295
- outgoing: {
296
- maxTotal: usdToBaseUnits(maxDailyUsd)
297
- }
298
- });
299
- changes.push(`Daily spending limit set to $${maxDailyUsd.toFixed(2)}`);
300
- logger2.info(`[x402] SET_PAYMENT_POLICY: maxTotal (daily) set to $${maxDailyUsd}`);
301
- }
302
- if (blockRecipient) {
303
- service.updatePolicy({
304
- outgoing: {
305
- blockedRecipients: [blockRecipient]
306
- }
307
- });
308
- changes.push(`Blocked recipient: ${blockRecipient}`);
309
- logger2.info(`[x402] SET_PAYMENT_POLICY: blocked recipient ${blockRecipient}`);
310
368
  }
311
- if (allowRecipient) {
312
- service.updatePolicy({
313
- outgoing: {
314
- allowedRecipients: [allowRecipient]
315
- }
316
- });
317
- changes.push(`Added to allowlist: ${allowRecipient}`);
318
- logger2.info(`[x402] SET_PAYMENT_POLICY: allowed recipient ${allowRecipient}`);
319
- }
320
- const summary = changes.join(`
321
- - `);
322
- if (callback) {
323
- await callback({
324
- text: `Payment policy updated:
325
- - ${summary}`,
326
- actions: []
327
- });
369
+ if (config.paymentAddress === "0x0000000000000000000000000000000000000000") {
370
+ warnings.push(`Config '${configName}': using zero address (0x0...0) - is this intentional?`);
328
371
  }
329
- return {
330
- success: true,
331
- text: `Payment policy updated: ${changes.join("; ")}`,
332
- data: {
333
- maxPerPaymentUsd: maxPerPaymentUsd ?? null,
334
- maxDailyUsd: maxDailyUsd ?? null,
335
- blockRecipient: blockRecipient ?? null,
336
- allowRecipient: allowRecipient ?? null
337
- }
338
- };
339
- } catch (err) {
340
- const errorMessage = err instanceof Error ? err.message : String(err);
341
- logger2.error(`[x402] SET_PAYMENT_POLICY: Failed to update policy: ${errorMessage}`);
342
- if (callback) {
343
- await callback({
344
- text: `Failed to update payment policy: ${errorMessage}`,
345
- actions: []
346
- });
347
- }
348
- return { success: false, error: errorMessage };
349
372
  }
350
- },
351
- examples: [
352
- [
353
- {
354
- name: "user",
355
- content: {
356
- text: "Set the maximum payment per transaction to $5"
357
- }
358
- },
359
- {
360
- name: "assistant",
361
- content: {
362
- text: "I'll set the per-transaction limit to $5.00.",
363
- actions: ["SET_PAYMENT_POLICY"]
364
- }
365
- }
366
- ],
367
- [
368
- {
369
- name: "user",
370
- content: {
371
- text: "Limit my daily spending to $50 and block payments to 0xDEAD...BEEF"
372
- }
373
- },
374
- {
375
- name: "assistant",
376
- content: {
377
- text: "I'll set the daily limit to $50 and block that address.",
378
- actions: ["SET_PAYMENT_POLICY"]
379
- }
373
+ if (config.assetReference && config.assetNamespace === "erc20") {
374
+ if (!/^0x[a-fA-F0-9]{40}$/.test(config.assetReference)) {
375
+ errors.push(`Config '${configName}': invalid ERC20 token address format`);
380
376
  }
381
- ],
382
- [
383
- {
384
- name: "user",
385
- content: {
386
- text: "Allow payments only to 0x1234567890abcdef1234567890abcdef12345678"
387
- }
388
- },
389
- {
390
- name: "assistant",
391
- content: {
392
- text: "Adding that address to the payment allowlist.",
393
- actions: ["SET_PAYMENT_POLICY"]
394
- }
377
+ }
378
+ if (paymentAddressIsBundledExample(config.network, config.paymentAddress)) {
379
+ if (false) {} else {
380
+ warnings.push(`Config '${configName}': paymentAddress matches the bundled dev example for ${config.network} — set env payout keys for real settlement.`);
395
381
  }
396
- ]
397
- ]
398
- };
399
-
400
- // actions/pay-for-service.ts
401
- import { logger as logger3 } from "@elizaos/core";
402
- var payForServiceAction = {
403
- name: "PAY_FOR_SERVICE",
404
- description: "Make a request to an x402-protected URL, automatically paying if required. Use when you need to access a paid API or service that uses the x402 payment protocol.",
405
- similes: [
406
- "pay for service",
407
- "x402 payment",
408
- "make a paid request",
409
- "access paid endpoint",
410
- "pay and fetch"
411
- ],
412
- parameters: [
413
- {
414
- name: "url",
415
- description: "The URL of the x402-protected service to access",
416
- required: true,
417
- schema: { type: "string" }
418
- },
419
- {
420
- name: "method",
421
- description: "HTTP method (GET, POST, etc.). Defaults to GET.",
422
- required: false,
423
- schema: {
424
- type: "string",
425
- enumValues: ["GET", "POST", "PUT", "DELETE"]
382
+ }
383
+ if (!BUILT_IN_NETWORKS.includes(config.network)) {
384
+ warnings.push(`Config '${configName}': using custom network '${config.network}' ` + `(not in built-in networks: ${BUILT_IN_NETWORKS.join(", ")})`);
385
+ }
386
+ } catch (error) {
387
+ errors.push(`Config '${configName}': ${error instanceof Error ? error.message : "unknown error"}`);
388
+ }
389
+ return { errors, warnings };
390
+ }
391
+ function validateX402Route(route, character, agentId) {
392
+ const errors = [];
393
+ const warnings = [];
394
+ const x402Route = route;
395
+ if (!route.path) {
396
+ errors.push(`Route missing 'path' property`);
397
+ return { errors, warnings };
398
+ }
399
+ const routePath = route.path;
400
+ if (x402Route.x402 == null) {
401
+ return { errors, warnings };
402
+ }
403
+ const cx = character?.settings?.x402;
404
+ const raw = x402Route.x402;
405
+ let priceInCents;
406
+ let paymentConfigs;
407
+ if (raw === true) {
408
+ priceInCents = cx?.defaultPriceInCents;
409
+ paymentConfigs = cx?.defaultPaymentConfigs;
410
+ if (priceInCents == null) {
411
+ errors.push(`${routePath}: x402: true requires character.settings.x402.defaultPriceInCents`);
412
+ }
413
+ if (!paymentConfigs?.length) {
414
+ errors.push(`${routePath}: x402: true requires character.settings.x402.defaultPaymentConfigs (non-empty array)`);
415
+ }
416
+ } else if (typeof raw === "object" && !Array.isArray(raw)) {
417
+ priceInCents = raw.priceInCents ?? cx?.defaultPriceInCents;
418
+ paymentConfigs = raw.paymentConfigs ?? cx?.defaultPaymentConfigs;
419
+ if (priceInCents == null) {
420
+ errors.push(`${routePath}: x402.priceInCents is required (or set character.settings.x402.defaultPriceInCents)`);
421
+ }
422
+ if (!paymentConfigs?.length) {
423
+ errors.push(`${routePath}: x402.paymentConfigs is required (or set character.settings.x402.defaultPaymentConfigs)`);
424
+ }
425
+ } else {
426
+ errors.push(`${routePath}: x402 must be true or a configuration object`);
427
+ }
428
+ if (priceInCents !== undefined && priceInCents !== null) {
429
+ if (typeof priceInCents !== "number") {
430
+ errors.push(`${routePath}: resolved x402.priceInCents must be a number`);
431
+ } else if (priceInCents <= 0) {
432
+ errors.push(`${routePath}: x402.priceInCents must be > 0`);
433
+ } else if (!Number.isInteger(priceInCents)) {
434
+ errors.push(`${routePath}: x402.priceInCents must be an integer (cents)`);
435
+ } else if (priceInCents > 1e4) {
436
+ warnings.push(`${routePath}: price is $${(priceInCents / 100).toFixed(2)} — is this intentional?`);
437
+ }
438
+ }
439
+ if (paymentConfigs && !Array.isArray(paymentConfigs)) {
440
+ errors.push(`${routePath}: x402.paymentConfigs must be an array`);
441
+ } else if (paymentConfigs?.length === 0) {
442
+ errors.push(`${routePath}: x402.paymentConfigs cannot be empty`);
443
+ } else if (paymentConfigs?.length) {
444
+ const availableConfigs = listX402Configs(agentId);
445
+ for (const configName of paymentConfigs) {
446
+ if (typeof configName !== "string") {
447
+ errors.push(`${routePath}: x402.paymentConfigs contains non-string value`);
448
+ } else if (!availableConfigs.includes(configName)) {
449
+ errors.push(`${routePath}: unknown payment config '${configName}'. Available: ${availableConfigs.join(", ")}`);
450
+ } else {
451
+ const configValidation = validatePaymentConfig(configName, agentId);
452
+ errors.push(...configValidation.errors.map((e) => `${routePath}: ${e}`));
453
+ warnings.push(...configValidation.warnings.map((w) => `${routePath}: ${w}`));
426
454
  }
427
- },
428
- {
429
- name: "body",
430
- description: "Optional request body as a JSON string (for POST/PUT)",
431
- required: false,
432
- schema: { type: "string" }
433
- }
434
- ],
435
- validate: async (runtime, message, state, options) => {
436
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
437
- const __avText = __avTextRaw.toLowerCase();
438
- const __avKeywords = ["pay", "for", "service"];
439
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
440
- const __avRegex = new RegExp("\\b(?:pay|for|service)\\b", "i");
441
- const __avRegexOk = __avRegex.test(__avText);
442
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
443
- const __avExpectedSource = "";
444
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
445
- const __avOptions = options && typeof options === "object" ? options : {};
446
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
447
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
448
- return false;
449
455
  }
450
- const __avLegacyValidate = async (runtime2) => {
451
- const service = runtime2.getService("x402_payment");
452
- return !!service && service.canMakePayments();
453
- };
454
- try {
455
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
456
- } catch {
456
+ }
457
+ if (!route.handler) {
458
+ errors.push(`${routePath}: route has x402 protection but no handler function`);
459
+ }
460
+ return { errors, warnings };
461
+ }
462
+ function validateEnvironment() {
463
+ const errors = [];
464
+ const warnings = [];
465
+ const health = getX402Health();
466
+ for (const network of health.networks) {
467
+ if (!network.configured || !network.address) {
468
+ warnings.push(`Network '${network.network}' not configured. ` + `Set ${network.network}_PUBLIC_KEY in .env to accept payments on this network.`);
469
+ }
470
+ }
471
+ if (!health.facilitator.configured) {
472
+ warnings.push("X402_FACILITATOR_URL not set. Direct blockchain verification will be used. " + "Consider setting up a facilitator for better UX.");
473
+ }
474
+ if (false) {}
475
+ return { errors, warnings };
476
+ }
477
+ function validateX402Startup(routes, character, options) {
478
+ const allErrors = [];
479
+ const allWarnings = [];
480
+ let protectedRouteCount = 0;
481
+ for (const route of routes) {
482
+ const x402Route = route;
483
+ if (x402Route.x402 != null) {
484
+ protectedRouteCount++;
485
+ const routeValidation = validateX402Route(route, character, options?.agentId);
486
+ allErrors.push(...routeValidation.errors);
487
+ allWarnings.push(...routeValidation.warnings);
488
+ }
489
+ }
490
+ if (protectedRouteCount > 0) {
491
+ const envValidation = validateEnvironment();
492
+ allErrors.push(...envValidation.errors);
493
+ allWarnings.push(...envValidation.warnings);
494
+ logger2.info(`[x402] validated ${protectedRouteCount}/${routes.length} protected route(s); ` + `configs=${listX402Configs(options?.agentId).length}, ` + `errors=${allErrors.length}, warnings=${allWarnings.length}`);
495
+ }
496
+ return {
497
+ valid: allErrors.length === 0,
498
+ errors: allErrors,
499
+ warnings: allWarnings
500
+ };
501
+ }
502
+ function validateAndThrowIfInvalid(routes, character, options) {
503
+ const result = validateX402Startup(routes, character, options);
504
+ if (!result.valid) {
505
+ throw new Error(`x402 Configuration Invalid (${result.errors.length} error${result.errors.length > 1 ? "s" : ""}):
506
+
507
+ ` + result.errors.map((e) => ` • ${e}`).join(`
508
+ `) + `
509
+
510
+ Please fix these errors and try again.`);
511
+ }
512
+ }
513
+
514
+ // src/x402-facilitator-binding.ts
515
+ function isFacilitatorBindingRelaxed() {
516
+ return process.env.X402_FACILITATOR_RELAXED_BINDING === "true" || process.env.X402_FACILITATOR_RELAXED_BINDING === "1";
517
+ }
518
+ function relaxedPayloadMatchesContext(data, ctx) {
519
+ if (typeof data.resource === "string" && data.resource !== ctx.resource) {
520
+ return false;
521
+ }
522
+ if (typeof data.routePath === "string" && data.routePath !== ctx.routePath) {
523
+ return false;
524
+ }
525
+ if (typeof data.route === "string" && data.route !== ctx.routePath) {
526
+ return false;
527
+ }
528
+ if (typeof data.priceInCents === "number" && Number.isFinite(data.priceInCents) && data.priceInCents !== ctx.priceInCents) {
529
+ return false;
530
+ }
531
+ if (typeof data.paymentConfig === "string") {
532
+ if (!ctx.paymentConfigNames.includes(data.paymentConfig)) {
457
533
  return false;
458
534
  }
459
- },
460
- handler: async (runtime, message, _state, options, callback) => {
461
- const service = runtime.getService("x402_payment");
462
- if (!service || !service.canMakePayments()) {
463
- logger3.warn("[x402] PAY_FOR_SERVICE: Service not available or inactive");
464
- if (callback) {
465
- await callback({
466
- text: "I'm unable to make payments right now. The x402 payment service is not configured or is inactive.",
467
- actions: []
468
- });
469
- }
470
- return { success: false, error: "x402 service not available" };
471
- }
472
- const params = options?.parameters;
473
- const url = params?.url ?? extractUrlFromMessage(message);
474
- const method = params?.method ?? "GET";
475
- const body = params?.body;
476
- if (!url) {
477
- logger3.warn("[x402] PAY_FOR_SERVICE: No URL provided");
478
- if (callback) {
479
- await callback({
480
- text: "I need a URL to make the paid request. Please provide the URL of the service you want me to access.",
481
- actions: []
482
- });
535
+ }
536
+ if (Array.isArray(data.paymentConfigs)) {
537
+ for (const n of data.paymentConfigs) {
538
+ if (typeof n === "string" && !ctx.paymentConfigNames.includes(n)) {
539
+ return false;
483
540
  }
484
- return { success: false, error: "No URL provided" };
485
541
  }
486
- logger3.info(`[x402] PAY_FOR_SERVICE: Requesting ${method} ${url}`);
487
- const payFetch = service.getFetchWithPayment();
488
- try {
489
- const init = { method };
490
- if (body && (method === "POST" || method === "PUT")) {
491
- init.body = body;
492
- init.headers = { "Content-Type": "application/json" };
493
- }
494
- const response = await payFetch(url, init);
495
- const contentType = response.headers.get("content-type") ?? "";
496
- let responseText;
497
- if (contentType.includes("application/json")) {
498
- const json = await response.json();
499
- responseText = JSON.stringify(json, null, 2);
500
- } else {
501
- responseText = await response.text();
502
- }
503
- const maxLen = 2000;
504
- const truncated = responseText.length > maxLen ? `${responseText.slice(0, maxLen)}...
505
- [Truncated - ${responseText.length} total characters]` : responseText;
506
- if (response.ok) {
507
- logger3.info(`[x402] PAY_FOR_SERVICE: Success (${response.status}) from ${url}`);
508
- if (callback) {
509
- await callback({
510
- text: `Successfully accessed ${url} (HTTP ${response.status}):
542
+ }
543
+ return true;
544
+ }
545
+ function strictPaymentConfigOk(data, ctx) {
546
+ if (typeof data.paymentConfig === "string") {
547
+ return ctx.paymentConfigNames.includes(data.paymentConfig);
548
+ }
549
+ if (Array.isArray(data.paymentConfigs) && data.paymentConfigs.length > 0) {
550
+ const names = data.paymentConfigs.filter((x) => typeof x === "string");
551
+ if (names.length === 0)
552
+ return false;
553
+ return names.every((n) => ctx.paymentConfigNames.includes(n));
554
+ }
555
+ return false;
556
+ }
557
+ function strictPayloadMatchesContext(data, ctx) {
558
+ if (typeof data.resource !== "string" || data.resource !== ctx.resource) {
559
+ return false;
560
+ }
561
+ const routeOk = typeof data.routePath === "string" && data.routePath === ctx.routePath || typeof data.route === "string" && data.route === ctx.routePath;
562
+ if (!routeOk) {
563
+ return false;
564
+ }
565
+ if (typeof data.priceInCents !== "number" || !Number.isFinite(data.priceInCents) || data.priceInCents !== ctx.priceInCents) {
566
+ return false;
567
+ }
568
+ if (!strictPaymentConfigOk(data, ctx)) {
569
+ return false;
570
+ }
571
+ return true;
572
+ }
573
+ function facilitatorVerifyResponseMatchesRoute(data, ctx, relaxed) {
574
+ return relaxed ? relaxedPayloadMatchesContext(data, ctx) : strictPayloadMatchesContext(data, ctx);
575
+ }
511
576
 
512
- ${truncated}`,
513
- actions: []
514
- });
515
- }
516
- return {
517
- success: true,
518
- text: `Payment and request successful for ${url}`,
519
- data: {
520
- status: response.status,
521
- url,
522
- responsePreview: truncated
523
- }
524
- };
525
- } else {
526
- logger3.warn(`[x402] PAY_FOR_SERVICE: Request returned ${response.status} from ${url}`);
527
- if (callback) {
528
- await callback({
529
- text: `Request to ${url} returned HTTP ${response.status}:
577
+ // src/x402-replay-guard.ts
578
+ import { logger as logger4 } from "@elizaos/core";
530
579
 
531
- ${truncated}`,
532
- actions: []
533
- });
580
+ // src/x402-replay-durable.ts
581
+ import { createHash, randomUUID } from "node:crypto";
582
+ import { logger as logger3 } from "@elizaos/core";
583
+ import { sql } from "drizzle-orm";
584
+ function sha256Utf8(s) {
585
+ return createHash("sha256").update(s, "utf8").digest("hex");
586
+ }
587
+ function durableReplayCacheKey(agentId, replayKey) {
588
+ const agent = agentId && agentId.trim().length > 0 ? agentId.trim() : "_";
589
+ return `x402:replay:v1:${sha256Utf8(`${agent}::${replayKey}`)}`;
590
+ }
591
+ function replayReservationTtlMs() {
592
+ const raw = process.env.X402_REPLAY_RESERVATION_TTL_MS;
593
+ const n = Number.parseInt(raw ?? "120000", 10);
594
+ return Number.isFinite(n) && n > 0 ? n : 120000;
595
+ }
596
+ function resultHadRows(result) {
597
+ if (Array.isArray(result))
598
+ return result.length > 0;
599
+ if (typeof result === "object" && result !== null) {
600
+ const rows = result.rows;
601
+ if (Array.isArray(rows))
602
+ return rows.length > 0;
603
+ const rowCount = result.rowCount;
604
+ if (typeof rowCount === "number")
605
+ return rowCount > 0;
606
+ }
607
+ return false;
608
+ }
609
+ async function getSqlDb(runtime) {
610
+ const db = await runtime.adapter?.getConnection?.();
611
+ if (db && typeof db === "object" && typeof db.execute === "function") {
612
+ return db;
613
+ }
614
+ return null;
615
+ }
616
+ async function insertInflightReservation(db, agentId, cacheKey, payload) {
617
+ const result = await db.execute(sql`
618
+ INSERT INTO cache (key, agent_id, value)
619
+ VALUES (${cacheKey}, ${agentId}, ${JSON.stringify(payload)}::jsonb)
620
+ ON CONFLICT (key, agent_id) DO NOTHING
621
+ RETURNING key
622
+ `);
623
+ return resultHadRows(result);
624
+ }
625
+ async function stealExpiredInflightReservation(db, agentId, cacheKey, payload, now) {
626
+ const result = await db.execute(sql`
627
+ UPDATE cache
628
+ SET value = ${JSON.stringify(payload)}::jsonb
629
+ WHERE key = ${cacheKey}
630
+ AND agent_id = ${agentId}
631
+ AND value->>'state' = 'inflight'
632
+ AND COALESCE((value->>'expiresAt')::bigint, 0) <= ${now}
633
+ RETURNING key
634
+ `);
635
+ return resultHadRows(result);
636
+ }
637
+ async function releaseSqlReservations(db, agentId, cacheKeys, owner) {
638
+ for (const cacheKey of cacheKeys) {
639
+ await db.execute(sql`
640
+ DELETE FROM cache
641
+ WHERE key = ${cacheKey}
642
+ AND agent_id = ${agentId}
643
+ AND value->>'state' = 'inflight'
644
+ AND value->>'owner' = ${owner}
645
+ `);
646
+ }
647
+ }
648
+ async function commitSqlReservations(db, agentId, cacheKeys, owner) {
649
+ const payload = {
650
+ state: "consumed",
651
+ consumedAt: Date.now()
652
+ };
653
+ for (const cacheKey of cacheKeys) {
654
+ const result = await db.execute(sql`
655
+ UPDATE cache
656
+ SET value = ${JSON.stringify(payload)}::jsonb
657
+ WHERE key = ${cacheKey}
658
+ AND agent_id = ${agentId}
659
+ AND value->>'state' = 'inflight'
660
+ AND value->>'owner' = ${owner}
661
+ RETURNING key
662
+ `);
663
+ if (!resultHadRows(result)) {
664
+ logger3.error(`[x402] durable replay: failed to commit reserved replay key ${cacheKey}`);
665
+ }
666
+ }
667
+ }
668
+ function isConsumed(value) {
669
+ if (!value)
670
+ return false;
671
+ return value.state === "consumed" || "consumedAt" in value;
672
+ }
673
+ async function durableReplayTryReserve(runtime, agentId, keys) {
674
+ if (keys.length === 0)
675
+ return { ok: true, owner: randomUUID(), atomic: true };
676
+ const db = await getSqlDb(runtime);
677
+ const resolvedAgentId = agentId && agentId.trim().length > 0 ? agentId.trim() : runtime.agentId ? String(runtime.agentId) : undefined;
678
+ if (db && resolvedAgentId) {
679
+ const owner2 = randomUUID();
680
+ const now2 = Date.now();
681
+ const payload2 = {
682
+ state: "inflight",
683
+ owner: owner2,
684
+ reservedAt: now2,
685
+ expiresAt: now2 + replayReservationTtlMs()
686
+ };
687
+ const acquired = [];
688
+ try {
689
+ for (const replayKey of keys) {
690
+ const cacheKey = durableReplayCacheKey(resolvedAgentId, replayKey);
691
+ if (await insertInflightReservation(db, resolvedAgentId, cacheKey, payload2) || await stealExpiredInflightReservation(db, resolvedAgentId, cacheKey, payload2, now2)) {
692
+ acquired.push(cacheKey);
693
+ continue;
534
694
  }
535
- return {
536
- success: false,
537
- error: `HTTP ${response.status}`,
538
- data: {
539
- status: response.status,
540
- url,
541
- responsePreview: truncated
542
- }
543
- };
695
+ await releaseSqlReservations(db, resolvedAgentId, acquired, owner2);
696
+ return { ok: false };
544
697
  }
698
+ return { ok: true, owner: owner2, atomic: true };
545
699
  } catch (err) {
546
- const errorMessage = err instanceof Error ? err.message : String(err);
547
- logger3.error(`[x402] PAY_FOR_SERVICE: Request failed: ${errorMessage}`);
548
- if (callback) {
549
- await callback({
550
- text: `Failed to access ${url}: ${errorMessage}`,
551
- actions: []
552
- });
553
- }
554
- return { success: false, error: errorMessage };
700
+ await releaseSqlReservations(db, resolvedAgentId, acquired, owner2).catch(() => {});
701
+ logger3.error(`[x402] durable replay: atomic reservation failed: ${err instanceof Error ? err.message : String(err)}`);
702
+ return { ok: false };
703
+ }
704
+ }
705
+ const owner = randomUUID();
706
+ for (const replayKey of keys) {
707
+ const cacheKey = durableReplayCacheKey(agentId, replayKey);
708
+ const v = await runtime.getCache(cacheKey);
709
+ if (isConsumed(v))
710
+ return { ok: false };
711
+ if (v?.state === "inflight" && v.expiresAt > Date.now()) {
712
+ return { ok: false };
713
+ }
714
+ }
715
+ const now = Date.now();
716
+ const payload = {
717
+ state: "inflight",
718
+ owner,
719
+ reservedAt: now,
720
+ expiresAt: now + replayReservationTtlMs()
721
+ };
722
+ for (const replayKey of keys) {
723
+ await runtime.setCache(durableReplayCacheKey(agentId, replayKey), payload);
724
+ }
725
+ return { ok: true, owner, atomic: false };
726
+ }
727
+ async function durableReplayAbortReservation(runtime, agentId, keys, owner) {
728
+ if (!owner || keys.length === 0)
729
+ return;
730
+ const db = await getSqlDb(runtime);
731
+ const resolvedAgentId = agentId && agentId.trim().length > 0 ? agentId.trim() : runtime.agentId ? String(runtime.agentId) : undefined;
732
+ if (db && resolvedAgentId) {
733
+ await releaseSqlReservations(db, resolvedAgentId, keys.map((k) => durableReplayCacheKey(resolvedAgentId, k)), owner);
734
+ return;
735
+ }
736
+ for (const replayKey of keys) {
737
+ const cacheKey = durableReplayCacheKey(agentId, replayKey);
738
+ const v = await runtime.getCache(cacheKey);
739
+ if (v?.state === "inflight" && v.owner === owner) {
740
+ await runtime.deleteCache(cacheKey);
555
741
  }
556
- },
557
- examples: [
558
- [
559
- {
560
- name: "user",
561
- content: {
562
- text: "Can you fetch the data from https://api.example.com/premium/data? It's a paid API."
563
- }
564
- },
565
- {
566
- name: "assistant",
567
- content: {
568
- text: "I'll access that paid API for you now.",
569
- actions: ["PAY_FOR_SERVICE"]
570
- }
571
- }
572
- ],
573
- [
574
- {
575
- name: "user",
576
- content: {
577
- text: "Please make a paid request to https://weather.paid-api.com/forecast"
578
- }
579
- },
580
- {
581
- name: "assistant",
582
- content: {
583
- text: "Making a paid request to the weather API.",
584
- actions: ["PAY_FOR_SERVICE"]
585
- }
586
- }
587
- ]
588
- ]
589
- };
590
- function extractUrlFromMessage(message) {
591
- const text = typeof message.content === "string" ? message.content : message.content?.text;
592
- if (!text)
742
+ }
743
+ }
744
+ async function durableReplayCommitReservation(runtime, agentId, keys, owner) {
745
+ if (keys.length === 0)
746
+ return;
747
+ const db = await getSqlDb(runtime);
748
+ const resolvedAgentId = agentId && agentId.trim().length > 0 ? agentId.trim() : runtime.agentId ? String(runtime.agentId) : undefined;
749
+ if (db && resolvedAgentId && owner) {
750
+ await commitSqlReservations(db, resolvedAgentId, keys.map((k) => durableReplayCacheKey(resolvedAgentId, k)), owner);
593
751
  return;
594
- const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/g;
595
- const matches = text.match(urlRegex);
596
- return matches?.[0];
752
+ }
753
+ const payload = {
754
+ state: "consumed",
755
+ consumedAt: Date.now()
756
+ };
757
+ for (const replayKey of keys) {
758
+ const ok = await runtime.setCache(durableReplayCacheKey(agentId, replayKey), payload);
759
+ if (!ok) {
760
+ logger3.error(`[x402] durable replay: setCache failed for replay key ${replayKey.slice(0, 80)} (payment may be retryable if this persists)`);
761
+ }
762
+ }
597
763
  }
598
764
 
599
- // providers/payment-balance.ts
600
- var paymentBalanceProvider = {
601
- name: "x402_payment_status",
602
- description: "Current x402 payment status including wallet, spending, and earning summary",
603
- dynamic: true,
604
- get: async (runtime, _message, _state) => {
605
- const service = runtime.getService("x402_payment");
606
- if (!service || !service.isActive()) {
607
- return {
608
- text: `[Payment Status]
609
- Payments: Inactive (no wallet configured)`,
610
- values: {
611
- x402Active: false
612
- }
613
- };
765
+ // src/x402-replay-guard.ts
766
+ var inflight = new Set;
767
+ var consumedMemory = new Map;
768
+ var durableReservationOwners = new Map;
769
+ function replayWindowMs() {
770
+ const raw = process.env.X402_REPLAY_WINDOW_MS ?? process.env.X402_REPLAY_TTL_MS;
771
+ const n = Number.parseInt(raw ?? "600000", 10);
772
+ return Number.isFinite(n) && n > 0 ? n : 600000;
773
+ }
774
+ function pruneConsumedMemory(now) {
775
+ for (const [k, exp] of consumedMemory) {
776
+ if (exp <= now)
777
+ consumedMemory.delete(k);
778
+ }
779
+ }
780
+ function isDurableReplayEnabled() {
781
+ const v = process.env.X402_REPLAY_DURABLE?.trim().toLowerCase();
782
+ if (v === "0" || v === "false" || v === "off")
783
+ return false;
784
+ return true;
785
+ }
786
+ async function replayGuardTryBegin(keys, runtime, agentId) {
787
+ if (keys.length === 0)
788
+ return true;
789
+ const now = Date.now();
790
+ const useDurable = isDurableReplayEnabled() && runtime != null;
791
+ try {
792
+ let durableOwner = null;
793
+ if (useDurable) {
794
+ const reservation = await durableReplayTryReserve(runtime, agentId, keys);
795
+ if (!reservation.ok)
796
+ return false;
797
+ durableOwner = reservation.owner;
798
+ } else {
799
+ pruneConsumedMemory(now);
800
+ for (const k of keys) {
801
+ const exp = consumedMemory.get(k);
802
+ if (exp != null && exp > now)
803
+ return false;
804
+ }
614
805
  }
615
- const walletAddress = service.getWalletAddress();
616
- const network = service.getNetwork();
617
- const summary = await service.getSummary(ONE_DAY_MS);
618
- const netAmount = summary.totalEarned - summary.totalSpent;
619
- const netDisplay = netAmount < 0n ? `-${formatUsd(-netAmount)}` : `+${formatUsd(netAmount)}`;
620
- const statusLines = [
621
- "[Payment Status]",
622
- `Wallet: ${walletAddress ? truncateAddress(walletAddress) : "N/A"} (${network})`,
623
- `24h Spent: ${formatUsd(summary.totalSpent)} (${summary.outgoingCount} txns)`,
624
- `24h Earned: ${formatUsd(summary.totalEarned)} (${summary.incomingCount} txns)`,
625
- `Net: ${netDisplay}`,
626
- `Circuit Breaker: ${service.getCircuitBreakerState()}`
627
- ];
628
- return {
629
- text: statusLines.join(`
630
- `),
631
- values: {
632
- x402Active: true,
633
- x402Wallet: walletAddress ?? "",
634
- x402Network: network,
635
- x402TotalSpent: formatUsd(summary.totalSpent),
636
- x402TotalEarned: formatUsd(summary.totalEarned),
637
- x402OutgoingCount: summary.outgoingCount,
638
- x402IncomingCount: summary.incomingCount
806
+ for (const k of keys) {
807
+ if (inflight.has(k)) {
808
+ if (durableOwner && runtime) {
809
+ await durableReplayAbortReservation(runtime, agentId, keys, durableOwner);
810
+ }
811
+ return false;
639
812
  }
640
- };
813
+ }
814
+ if (durableOwner) {
815
+ for (const k of keys)
816
+ durableReservationOwners.set(k, durableOwner);
817
+ }
818
+ for (const k of keys)
819
+ inflight.add(k);
820
+ return true;
821
+ } catch (err) {
822
+ logger4.error(`[x402] replayGuardTryBegin failed: ${err instanceof Error ? err.message : String(err)}`);
823
+ return false;
641
824
  }
642
- };
643
-
644
- // routes/agent-card.ts
645
- import { logger as logger4 } from "@elizaos/core";
646
-
647
- // networks.ts
648
- var NETWORK_REGISTRY = {
649
- base: {
650
- caip2: "eip155:8453",
651
- chainId: 8453,
652
- name: "Base",
653
- usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
654
- usdcDomainName: "USDC",
655
- usdcPermitVersion: "2",
656
- rpcUrl: "https://mainnet.base.org"
657
- },
658
- "base-sepolia": {
659
- caip2: "eip155:84532",
660
- chainId: 84532,
661
- name: "Base Sepolia",
662
- usdcAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
663
- usdcDomainName: "USDC",
664
- usdcPermitVersion: "2",
665
- rpcUrl: "https://sepolia.base.org"
666
- },
667
- ethereum: {
668
- caip2: "eip155:1",
669
- chainId: 1,
670
- name: "Ethereum",
671
- usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
672
- usdcDomainName: "USD Coin",
673
- usdcPermitVersion: "2",
674
- rpcUrl: "https://cloudflare-eth.com"
675
- },
676
- sepolia: {
677
- caip2: "eip155:11155111",
678
- chainId: 11155111,
679
- name: "Sepolia",
680
- usdcAddress: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
681
- usdcDomainName: "USDC",
682
- usdcPermitVersion: "2",
683
- rpcUrl: "https://rpc.sepolia.org"
825
+ }
826
+ function replayGuardAbort(keys) {
827
+ for (const k of keys) {
828
+ inflight.delete(k);
829
+ durableReservationOwners.delete(k);
684
830
  }
685
- };
686
- function resolveNetwork(key) {
687
- const info = NETWORK_REGISTRY[key];
688
- if (!info) {
689
- const supported = Object.keys(NETWORK_REGISTRY).join(", ");
690
- throw new Error(`Unknown network "${key}". Supported: ${supported}`);
831
+ }
832
+ async function replayGuardAbortAsync(keys, runtime, agentId) {
833
+ const owner = keys.map((k) => durableReservationOwners.get(k)).find((x) => typeof x === "string");
834
+ replayGuardAbort(keys);
835
+ if (owner && runtime) {
836
+ await durableReplayAbortReservation(runtime, agentId, keys, owner);
691
837
  }
692
- return info;
693
838
  }
694
- function networkKeyFromCaip2(caip2) {
695
- for (const [key, info] of Object.entries(NETWORK_REGISTRY)) {
696
- if (info.caip2 === caip2) {
697
- return key;
839
+ async function replayGuardCommit(keys, runtime, agentId) {
840
+ const useDurable = isDurableReplayEnabled() && runtime != null;
841
+ const exp = Date.now() + replayWindowMs();
842
+ let owner;
843
+ let ownerConsistent = true;
844
+ for (const k of keys) {
845
+ const o = durableReservationOwners.get(k);
846
+ if (typeof o !== "string")
847
+ continue;
848
+ if (owner === undefined) {
849
+ owner = o;
850
+ } else if (owner !== o) {
851
+ ownerConsistent = false;
852
+ break;
698
853
  }
699
854
  }
700
- return;
855
+ for (const k of keys) {
856
+ inflight.delete(k);
857
+ durableReservationOwners.delete(k);
858
+ }
859
+ if (useDurable && keys.length > 0 && runtime) {
860
+ await durableReplayCommitReservation(runtime, agentId, keys, ownerConsistent ? owner : undefined);
861
+ } else if (!useDurable) {
862
+ for (const k of keys)
863
+ consumedMemory.set(k, exp);
864
+ }
701
865
  }
702
866
 
703
- // routes/agent-card.ts
704
- async function handleAgentCard(req, res, runtime) {
705
- const service = runtime.getService("x402_payment");
706
- if (!service || !service.isActive()) {
707
- res.status(503).json({ error: "x402 service not active" });
867
+ // src/x402-replay-keys.ts
868
+ import { createHash as createHash2 } from "node:crypto";
869
+ function sha256Utf82(s) {
870
+ return createHash2("sha256").update(s, "utf8").digest("hex");
871
+ }
872
+ function paymentIdReplayKey(paymentId) {
873
+ const cleaned = paymentId.trim();
874
+ if (!/^[a-zA-Z0-9_-]+$/.test(cleaned) || cleaned.length > 128)
875
+ return null;
876
+ return `fac:${sha256Utf82(cleaned)}`;
877
+ }
878
+ function looksMostlyPrintableAscii(s) {
879
+ if (!s || s.length > 1e5)
880
+ return false;
881
+ let ok = 0;
882
+ for (let i = 0;i < s.length; i++) {
883
+ const code = s.charCodeAt(i);
884
+ if (code === 9 || code === 10 || code === 13 || code >= 32 && code < 127) {
885
+ ok++;
886
+ }
887
+ }
888
+ return ok / s.length > 0.85;
889
+ }
890
+ function tryBase64Utf8(proof) {
891
+ const t = proof.trim();
892
+ if (t.length < 8 || !/^[A-Za-z0-9+/=_-]+$/.test(t.replace(/\s/g, ""))) {
893
+ return null;
894
+ }
895
+ const buf = Buffer.from(t, "base64");
896
+ if (buf.length === 0)
897
+ return null;
898
+ const decoded = buf.toString("utf8");
899
+ if (!decoded || decoded.includes("\x00"))
900
+ return null;
901
+ if (!looksMostlyPrintableAscii(decoded))
902
+ return null;
903
+ return decoded;
904
+ }
905
+ function decodePaymentProofForParsing(proof) {
906
+ return tryBase64Utf8(proof) ?? proof;
907
+ }
908
+ function addEvmTxHashes(s, into) {
909
+ const matches = s.match(/0x[a-fA-F0-9]{64}/g);
910
+ if (!matches)
708
911
  return;
912
+ for (const h of matches)
913
+ into.add(`evm-tx:${h.toLowerCase()}`);
914
+ }
915
+ function addSolanaTxSignatures(s, into) {
916
+ const parts = s.split(":");
917
+ if (parts.length >= 3 && parts[0]?.toUpperCase() === "SOLANA") {
918
+ const sig = parts[2]?.trim();
919
+ if (sig && /^[1-9A-HJ-NP-Za-km-z]{87,88}$/.test(sig)) {
920
+ into.add(`sol-tx:${sig}`);
921
+ }
709
922
  }
710
- const walletAddress = service.getWalletAddress() ?? "";
711
- const networkKey = service.getNetwork();
712
- const facilitatorUrl = service.getFacilitatorUrl();
713
- let caip2Network;
923
+ const trimmed = s.trim().split(/\s+/)[0] ?? "";
924
+ if (/^[1-9A-HJ-NP-Za-km-z]{87,88}$/.test(trimmed)) {
925
+ into.add(`sol-tx:${trimmed}`);
926
+ }
927
+ }
928
+ function addEip712StableKey(s, into) {
929
+ let obj;
714
930
  try {
715
- const networkInfo = resolveNetwork(networkKey);
716
- caip2Network = networkInfo.caip2;
717
- } catch (err) {
718
- caip2Network = networkKey;
719
- const msg = err instanceof Error ? err.message : String(err);
720
- logger4.warn("[x402] Agent card: could not resolve network '" + networkKey + "': " + msg);
721
- }
722
- const character = runtime.character;
723
- const agentName = character?.name ?? "ElizaOS Agent";
724
- const agentDescription = (Array.isArray(character?.bio) ? character.bio[0] : character?.bio) ?? "An ElizaOS agent with x402 payment capabilities";
725
- const configuredUrl = String(runtime.getSetting("X402_AGENT_URL") ?? "");
726
- let agentUrl = configuredUrl;
727
- if (!agentUrl && req.headers) {
728
- const host = (Array.isArray(req.headers.host) ? req.headers.host[0] : req.headers.host) ?? "";
729
- const proto = (Array.isArray(req.headers["x-forwarded-proto"]) ? req.headers["x-forwarded-proto"][0] : req.headers["x-forwarded-proto"]) ?? "https";
730
- if (host) {
731
- agentUrl = `${proto}://${host}`;
732
- }
733
- }
734
- const skills = [];
735
- const plugins = runtime.plugins ?? [];
736
- for (const plugin of plugins) {
737
- if (!plugin.routes)
738
- continue;
739
- for (const route of plugin.routes) {
740
- const x402Config = "x402" in route ? route.x402 : undefined;
741
- if (x402Config) {
742
- skills.push({
743
- name: ("name" in route ? route.name : undefined) ?? route.path,
744
- description: x402Config.description ?? `Paid endpoint: ${route.path}`,
745
- path: route.path,
746
- price: x402Config.price,
747
- network: x402Config.network
748
- });
749
- }
750
- }
931
+ obj = JSON.parse(s);
932
+ } catch {
933
+ return;
751
934
  }
752
- const card = {
753
- protocolVersion: "1.0",
754
- name: agentName,
755
- description: agentDescription,
756
- url: agentUrl,
757
- capabilities: {
758
- x402Payments: true
759
- },
760
- payments: [
761
- {
762
- method: "x402",
763
- payee: walletAddress,
764
- network: caip2Network,
765
- facilitatorUrl
766
- }
767
- ],
768
- skills
769
- };
770
- res.status(200).json(card);
771
- }
772
- var agentCardRoute = {
773
- type: "GET",
774
- path: "/.well-known/agent-card.json",
775
- name: "x402-agent-card",
776
- public: true,
777
- handler: handleAgentCard
778
- };
779
-
780
- // services/x402-service.ts
781
- import { Service } from "@elizaos/core";
782
- import { logger as logger5 } from "@elizaos/core";
783
-
784
- // client/signer.ts
785
- import { createPublicClient, http } from "viem";
786
- import { privateKeyToAccount } from "viem/accounts";
787
- var PERMIT_TYPES = {
788
- Permit: [
789
- { name: "owner", type: "address" },
790
- { name: "spender", type: "address" },
791
- { name: "value", type: "uint256" },
792
- { name: "nonce", type: "uint256" },
793
- { name: "deadline", type: "uint256" }
794
- ]
795
- };
935
+ if (typeof obj !== "object" || obj === null)
936
+ return;
937
+ const root = obj;
938
+ const payload = root.payload;
939
+ const auth = payload?.authorization ?? root.authorization;
940
+ if (!auth || typeof auth !== "object")
941
+ return;
942
+ const domain = payload?.domain ?? root.domain;
943
+ if (typeof auth.from !== "string" || typeof auth.to !== "string" || typeof auth.value !== "string" || typeof auth.nonce !== "string") {
944
+ return;
945
+ }
946
+ const contract = domain && typeof domain.verifyingContract === "string" ? domain.verifyingContract.toLowerCase() : "";
947
+ const chainId = domain && typeof domain.chainId === "number" ? domain.chainId : -1;
948
+ const stable = JSON.stringify({
949
+ c: contract,
950
+ ch: chainId,
951
+ f: auth.from.toLowerCase(),
952
+ t: auth.to.toLowerCase(),
953
+ v: auth.value,
954
+ n: auth.nonce
955
+ });
956
+ into.add(`eip712:${sha256Utf82(stable)}`);
957
+ }
958
+ function replayKeysFromProofString(proof) {
959
+ const keys = new Set;
960
+ const variants = new Set([proof]);
961
+ const decoded = tryBase64Utf8(proof);
962
+ if (decoded)
963
+ variants.add(decoded);
964
+ for (const v of variants) {
965
+ addEvmTxHashes(v, keys);
966
+ addSolanaTxSignatures(v, keys);
967
+ addEip712StableKey(v, keys);
968
+ }
969
+ return [...keys];
970
+ }
971
+ function collectReplayKeysToCheck(paymentProof, paymentId) {
972
+ const keys = new Set;
973
+ if (paymentId) {
974
+ const pk = paymentIdReplayKey(paymentId);
975
+ if (pk)
976
+ keys.add(pk);
977
+ }
978
+ if (paymentProof) {
979
+ for (const k of replayKeysFromProofString(paymentProof))
980
+ keys.add(k);
981
+ }
982
+ return [...keys];
983
+ }
796
984
 
797
- class EvmPaymentSigner {
798
- account;
799
- network;
800
- constructor(privateKey, network) {
801
- const key = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
802
- this.account = privateKeyToAccount(key);
803
- this.network = network;
804
- }
805
- get address() {
806
- return this.account.address;
807
- }
808
- get networkId() {
809
- return resolveNetwork(this.network).caip2;
810
- }
811
- async signPermit(params) {
812
- const networkInfo = resolveNetwork(this.network);
813
- const domain = {
814
- name: networkInfo.usdcDomainName,
815
- version: networkInfo.usdcPermitVersion,
816
- chainId: BigInt(networkInfo.chainId),
817
- verifyingContract: networkInfo.usdcAddress
818
- };
819
- const message = {
820
- owner: this.account.address,
821
- spender: params.spender,
822
- value: params.value,
823
- nonce: params.nonce,
824
- deadline: params.deadline
825
- };
826
- const signature = await this.account.signTypedData({
827
- domain,
828
- types: PERMIT_TYPES,
829
- primaryType: "Permit",
830
- message
831
- });
832
- const raw = signature.slice(2);
833
- const r = `0x${raw.slice(0, 64)}`;
834
- const s = `0x${raw.slice(64, 128)}`;
835
- const v = parseInt(raw.slice(128, 130), 16);
836
- return { v, r, s };
837
- }
838
- async queryOnChainNonce(usdcAddress, rpcUrl, chainId) {
839
- const client = createPublicClient({
840
- transport: http(rpcUrl)
841
- });
842
- const result = await client.readContract({
843
- address: usdcAddress,
844
- abi: [{
845
- inputs: [{ name: "owner", type: "address" }],
846
- name: "nonces",
847
- outputs: [{ name: "", type: "uint256" }],
848
- stateMutability: "view",
849
- type: "function"
850
- }],
851
- functionName: "nonces",
852
- args: [this.account.address]
853
- });
854
- return result;
855
- }
856
- async buildPaymentHeader(requirement) {
857
- const networkInfo = resolveNetwork(this.network);
858
- const amount = BigInt(requirement.maxAmountRequired);
859
- const tokenName = requirement.extra?.name ?? networkInfo.usdcDomainName;
860
- const tokenVersion = requirement.extra?.version ?? networkInfo.usdcPermitVersion;
861
- let nonce;
862
- if (requirement.extra?.nonce !== undefined) {
863
- nonce = BigInt(requirement.extra.nonce);
864
- } else {
865
- nonce = await this.queryOnChainNonce(requirement.asset, networkInfo.rpcUrl, networkInfo.chainId);
866
- }
867
- const deadline = BigInt(Math.floor(Date.now() / 1000) + requirement.maxTimeoutSeconds);
868
- const spender = requirement.payTo;
869
- const domain = {
870
- name: tokenName,
871
- version: tokenVersion,
872
- chainId: BigInt(networkInfo.chainId),
873
- verifyingContract: requirement.asset
874
- };
875
- const message = {
876
- owner: this.account.address,
877
- spender,
878
- value: amount,
879
- nonce,
880
- deadline
881
- };
882
- const signature = await this.account.signTypedData({
883
- domain,
884
- types: PERMIT_TYPES,
885
- primaryType: "Permit",
886
- message
887
- });
888
- const payload = {
889
- x402Version: 2,
890
- accepted: {
891
- scheme: "upto",
892
- network: requirement.network,
893
- asset: requirement.asset,
894
- amount: requirement.maxAmountRequired,
895
- payTo: requirement.payTo
896
- },
897
- payload: {
898
- authorization: {
899
- from: this.account.address,
900
- to: requirement.payTo,
901
- value: requirement.maxAmountRequired,
902
- validBefore: deadline.toString(),
903
- nonce: nonce.toString()
904
- },
905
- signature
906
- }
985
+ // src/x402-resolve.ts
986
+ var X402_EVENT_PAYMENT_VERIFIED = "PAYMENT_VERIFIED";
987
+ var X402_EVENT_PAYMENT_REQUIRED = "PAYMENT_REQUIRED";
988
+ function readCharacterX402(settings) {
989
+ if (!settings || typeof settings !== "object")
990
+ return;
991
+ const raw = settings.x402;
992
+ if (!raw || typeof raw !== "object")
993
+ return;
994
+ return raw;
995
+ }
996
+ function resolveEffectiveX402(route, runtime) {
997
+ const cx = readCharacterX402(runtime.character?.settings);
998
+ const raw = route.x402;
999
+ if (raw === true) {
1000
+ if (cx?.defaultPriceInCents == null || !cx.defaultPaymentConfigs?.length)
1001
+ return null;
1002
+ return {
1003
+ priceInCents: cx.defaultPriceInCents,
1004
+ paymentConfigs: [...cx.defaultPaymentConfigs]
907
1005
  };
908
- const jsonString = JSON.stringify(payload);
909
- return Buffer.from(jsonString).toString("base64");
910
1006
  }
1007
+ if (raw && typeof raw === "object") {
1008
+ const price = raw.priceInCents ?? cx?.defaultPriceInCents;
1009
+ const configs = raw.paymentConfigs ?? cx?.defaultPaymentConfigs;
1010
+ if (price == null || !configs?.length)
1011
+ return null;
1012
+ return { priceInCents: price, paymentConfigs: [...configs] };
1013
+ }
1014
+ return null;
911
1015
  }
912
1016
 
913
- // client/fetch-with-payment.ts
914
- function generateId() {
915
- const timestamp = Date.now().toString(36);
916
- const random = Math.random().toString(36).substring(2, 10);
917
- return `x402_${timestamp}_${random}`;
918
- }
919
- function parsePaymentRequired(response) {
920
- const headerValue = response.headers.get("payment-required") ?? response.headers.get("x-402") ?? response.headers.get("x-payment-required");
921
- if (!headerValue) {
1017
+ // src/x402-standard-payment.ts
1018
+ function looksMostlyPrintableAscii2(s) {
1019
+ if (!s || s.length > 1e5)
1020
+ return false;
1021
+ let ok = 0;
1022
+ for (let i = 0;i < s.length; i++) {
1023
+ const code = s.charCodeAt(i);
1024
+ if (code === 9 || code === 10 || code === 13 || code >= 32 && code < 127) {
1025
+ ok++;
1026
+ }
1027
+ }
1028
+ return ok / s.length > 0.85;
1029
+ }
1030
+ function tryBase64Utf8Json(raw) {
1031
+ const t = raw.trim();
1032
+ if (t.length < 8 || !/^[A-Za-z0-9+/=_-]+$/.test(t.replace(/\s/g, ""))) {
922
1033
  return null;
923
1034
  }
1035
+ const buf = Buffer.from(t, "base64");
1036
+ if (buf.length === 0)
1037
+ return null;
1038
+ const decoded = buf.toString("utf8");
1039
+ if (!decoded || decoded.includes("\x00"))
1040
+ return null;
1041
+ if (!looksMostlyPrintableAscii2(decoded))
1042
+ return null;
924
1043
  try {
925
- const decoded = Buffer.from(headerValue, "base64").toString("utf-8");
926
1044
  return JSON.parse(decoded);
927
1045
  } catch {
1046
+ return null;
1047
+ }
1048
+ }
1049
+ function decodeXPaymentHeader(raw) {
1050
+ const t = raw.trim();
1051
+ if (!t)
1052
+ return null;
1053
+ if (t.startsWith("{") || t.startsWith("[")) {
928
1054
  try {
929
- return JSON.parse(headerValue);
1055
+ return JSON.parse(t);
930
1056
  } catch {
931
1057
  return null;
932
1058
  }
933
1059
  }
1060
+ return tryBase64Utf8Json(t);
1061
+ }
1062
+ function isX402StandardPaymentPayload(v) {
1063
+ if (typeof v !== "object" || v === null)
1064
+ return false;
1065
+ const o = v;
1066
+ if (typeof o.x402Version !== "number")
1067
+ return false;
1068
+ const acc = o.accepted;
1069
+ if (typeof acc !== "object" || acc === null)
1070
+ return false;
1071
+ const a = acc;
1072
+ if (typeof a.scheme !== "string")
1073
+ return false;
1074
+ if (typeof a.network !== "string")
1075
+ return false;
1076
+ if (typeof a.asset !== "string")
1077
+ return false;
1078
+ if (typeof a.amount !== "string" && typeof a.maxAmountRequired !== "string") {
1079
+ return false;
1080
+ }
1081
+ if (typeof a.payTo !== "string")
1082
+ return false;
1083
+ const pl = o.payload;
1084
+ if (typeof pl !== "object" || pl === null)
1085
+ return false;
1086
+ const p = pl;
1087
+ if (typeof p.signature !== "string")
1088
+ return false;
1089
+ const auth = p.authorization;
1090
+ if (typeof auth !== "object" || auth === null)
1091
+ return false;
1092
+ const u = auth;
1093
+ return typeof u.from === "string" && typeof u.to === "string" && typeof u.value === "string" && typeof u.nonce === "string" && typeof u.validBefore === "string";
1094
+ }
1095
+ function toStandardNetwork(network) {
1096
+ if (network === "BASE")
1097
+ return "eip155:8453";
1098
+ if (network === "POLYGON")
1099
+ return "eip155:137";
1100
+ if (network === "BSC")
1101
+ return "eip155:56";
1102
+ return "solana:mainnet";
1103
+ }
1104
+ function acceptedNetworkMatches(acceptedNetwork, cfg) {
1105
+ const n = acceptedNetwork.trim();
1106
+ if (cfg.network === "SOLANA") {
1107
+ return n === "solana" || n === "solana:mainnet" || n === "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" || n.toLowerCase().includes("solana");
1108
+ }
1109
+ const caip = toStandardNetwork(cfg.network);
1110
+ const short = toX402Network(cfg.network);
1111
+ return n === caip || n.toLowerCase() === short || n === `caip2:${caip}`;
1112
+ }
1113
+ function assetMatchesAccepted(acceptedAsset, cfg) {
1114
+ const a = acceptedAsset.trim().toLowerCase();
1115
+ const ref = cfg.assetReference.trim().toLowerCase();
1116
+ if (a === ref)
1117
+ return true;
1118
+ if (a.includes(ref))
1119
+ return true;
1120
+ if (ref.includes(a) && a.startsWith("0x"))
1121
+ return true;
1122
+ return false;
1123
+ }
1124
+ function standardAssetForConfig(cfg) {
1125
+ if (cfg.assetNamespace === "erc20") {
1126
+ return cfg.assetReference;
1127
+ }
1128
+ return cfg.assetReference;
1129
+ }
1130
+ function buildStandardPaymentRequiredAccept(params) {
1131
+ const cfg = getPaymentConfig(params.configName, params.agentId);
1132
+ const maxAmountRequired = atomicAmountForPriceInCents(params.priceInCents, cfg);
1133
+ const extra = {
1134
+ name: cfg.symbol?.toUpperCase() === "USDC" ? "USD Coin" : cfg.symbol || "Token",
1135
+ version: "2",
1136
+ paymentConfig: params.configName
1137
+ };
1138
+ return {
1139
+ scheme: "exact",
1140
+ network: toStandardNetwork(cfg.network),
1141
+ maxAmountRequired,
1142
+ resource: toResourceUrl(params.routePath),
1143
+ description: params.description,
1144
+ mimeType: "application/json",
1145
+ payTo: cfg.paymentAddress,
1146
+ maxTimeoutSeconds: 300,
1147
+ asset: standardAssetForConfig(cfg),
1148
+ extra
1149
+ };
1150
+ }
1151
+ function buildStandardPaymentRequired(params) {
1152
+ return {
1153
+ x402Version: 2,
1154
+ error: params.error,
1155
+ accepts: params.paymentConfigNames.map((configName) => buildStandardPaymentRequiredAccept({
1156
+ routePath: params.routePath,
1157
+ description: params.description,
1158
+ priceInCents: params.priceInCents,
1159
+ configName,
1160
+ agentId: params.agentId
1161
+ }))
1162
+ };
1163
+ }
1164
+ function buildFacilitatorPaymentRequirements(params) {
1165
+ const cfg = getPaymentConfig(params.configName, params.agentId);
1166
+ const amount = atomicAmountForPriceInCents(params.priceInCents, cfg);
1167
+ const network = toStandardNetwork(cfg.network);
1168
+ return {
1169
+ scheme: "exact",
1170
+ network,
1171
+ asset: standardAssetForConfig(cfg),
1172
+ amount,
1173
+ payTo: cfg.paymentAddress,
1174
+ maxTimeoutSeconds: 300,
1175
+ extra: {
1176
+ name: cfg.symbol?.toUpperCase() === "USDC" ? "USD Coin" : cfg.symbol || "Token",
1177
+ version: "2",
1178
+ resource: toResourceUrl(params.routePath)
1179
+ }
1180
+ };
1181
+ }
1182
+ function findMatchingPaymentConfigForStandardPayload(payload, paymentConfigNames, priceInCents, agentId) {
1183
+ const { accepted } = payload;
1184
+ if (accepted.scheme !== "exact" && accepted.scheme !== "upto") {
1185
+ return null;
1186
+ }
1187
+ const acceptedAmount = accepted.amount ?? accepted.maxAmountRequired;
1188
+ if (!acceptedAmount)
1189
+ return null;
1190
+ let payAmount;
1191
+ try {
1192
+ payAmount = BigInt(acceptedAmount);
1193
+ } catch {
1194
+ return null;
1195
+ }
1196
+ for (const name of paymentConfigNames) {
1197
+ const cfg = getPaymentConfig(name, agentId);
1198
+ if (!acceptedNetworkMatches(accepted.network, cfg))
1199
+ continue;
1200
+ if (!assetMatchesAccepted(accepted.asset, cfg))
1201
+ continue;
1202
+ if (accepted.payTo.trim().toLowerCase() !== cfg.paymentAddress.trim().toLowerCase()) {
1203
+ continue;
1204
+ }
1205
+ const required = BigInt(atomicAmountForPriceInCents(priceInCents, cfg));
1206
+ if (payAmount < required)
1207
+ continue;
1208
+ return { name, cfg };
1209
+ }
1210
+ return null;
1211
+ }
1212
+ function getFacilitatorEndpoint(runtime, endpoint) {
1213
+ const explicit = runtime.getSetting(endpoint === "verify" ? "X402_FACILITATOR_VERIFY_URL" : "X402_FACILITATOR_SETTLE_URL");
1214
+ if (typeof explicit === "string" && explicit.trim()) {
1215
+ return explicit.trim().replace(/\/$/, "");
1216
+ }
1217
+ const fuSetting = runtime.getSetting("X402_FACILITATOR_URL");
1218
+ const fu = typeof fuSetting === "string" && fuSetting.trim() ? fuSetting.trim() : "https://x402.elizacloud.ai/api/v1/x402";
1219
+ try {
1220
+ const clean = fu.replace(/\/$/, "");
1221
+ const u = new URL(clean);
1222
+ if (u.pathname.endsWith("/api/facilitator")) {
1223
+ return `${u.origin}/api/v1/x402/${endpoint}`;
1224
+ }
1225
+ if (u.pathname.endsWith(`/${endpoint}`))
1226
+ return clean;
1227
+ return `${clean}/${endpoint}`;
1228
+ } catch {
1229
+ return null;
1230
+ }
1231
+ }
1232
+ function getFacilitatorVerifyPostUrl(runtime) {
1233
+ return getFacilitatorEndpoint(runtime, "verify");
934
1234
  }
935
- function selectPaymentOption(accepts, signerNetworkId) {
936
- const matching = accepts.find((a) => a.network === signerNetworkId);
937
- return matching ?? null;
938
- }
939
- function createFetchWithPayment(options) {
940
- const { signer, policyEngine, circuitBreaker, storage, logger: logger5 } = options;
941
- return async function fetchWithPayment(input, init) {
942
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
943
- logger5.debug(`[x402] Making request to ${url}`);
944
- const initialResponse = await fetch(input, init);
945
- if (initialResponse.status !== 402) {
946
- return initialResponse;
947
- }
948
- logger5.info(`[x402] Received 402 Payment Required from ${url}`);
949
- const paymentRequired = parsePaymentRequired(initialResponse);
950
- if (!paymentRequired) {
951
- logger5.error("[x402] Could not parse payment requirement header from 402 response");
952
- return initialResponse;
953
- }
954
- if (!paymentRequired.accepts || paymentRequired.accepts.length === 0) {
955
- logger5.error("[x402] No payment options in 402 response");
956
- return initialResponse;
957
- }
958
- const requirement = selectPaymentOption(paymentRequired.accepts, signer.networkId);
959
- if (!requirement) {
960
- logger5.error("[x402] No compatible payment option found");
961
- return initialResponse;
962
- }
963
- const amount = BigInt(requirement.maxAmountRequired);
964
- logger5.info(`[x402] Payment required: ${amount} to ${requirement.payTo} on ${requirement.network}`);
965
- const policyResult = await policyEngine.evaluateOutgoing({
966
- amount,
967
- recipient: requirement.payTo,
968
- resource: url
1235
+ function getFacilitatorSettlePostUrl(runtime) {
1236
+ return getFacilitatorEndpoint(runtime, "settle");
1237
+ }
1238
+ async function verifyPaymentPayloadViaFacilitatorPost(runtime, paymentPayload, paymentRequirements) {
1239
+ const url = getFacilitatorVerifyPostUrl(runtime);
1240
+ if (!url) {
1241
+ return { ok: false, invalidReason: "no_facilitator_verify_url" };
1242
+ }
1243
+ try {
1244
+ const res = await fetch(url, {
1245
+ method: "POST",
1246
+ headers: {
1247
+ Accept: "application/json",
1248
+ "Content-Type": "application/json",
1249
+ "User-Agent": "ElizaOS-X402-Agent/1.0"
1250
+ },
1251
+ body: JSON.stringify({ paymentPayload, paymentRequirements }),
1252
+ signal: AbortSignal.timeout(15000)
969
1253
  });
970
- if (!policyResult.allowed) {
971
- logger5.warn(`[x402] Payment blocked by policy: ${policyResult.reason}`);
972
- return initialResponse;
1254
+ const text = await res.text();
1255
+ let body = {};
1256
+ if (text) {
1257
+ try {
1258
+ body = JSON.parse(text);
1259
+ } catch {
1260
+ return { ok: false, invalidReason: "invalid_verify_response_json" };
1261
+ }
1262
+ }
1263
+ if (!res.ok && res.status !== 400) {
1264
+ return {
1265
+ ok: false,
1266
+ invalidReason: `verify_http_${res.status}`
1267
+ };
973
1268
  }
974
- const breakerResult = circuitBreaker.check(amount);
975
- if (!breakerResult.allowed) {
976
- logger5.warn(`[x402] Payment blocked by circuit breaker: ${breakerResult.reason}`);
977
- return initialResponse;
1269
+ if (body.isValid === true) {
1270
+ return { ok: true, payer: body.payer };
978
1271
  }
979
- let paymentHeader;
980
- try {
981
- paymentHeader = await signer.buildPaymentHeader(requirement);
982
- } catch (err) {
983
- const message = err instanceof Error ? err.message : String(err);
984
- logger5.error(`[x402] Failed to sign payment: ${message}`);
985
- circuitBreaker.recordFailure();
986
- return initialResponse;
987
- }
988
- logger5.info("[x402] Retrying request with X-PAYMENT header");
989
- const retryHeaders = new Headers(init?.headers);
990
- retryHeaders.set("X-PAYMENT", paymentHeader);
991
- const retryInit = {
992
- ...init,
993
- headers: retryHeaders
1272
+ return {
1273
+ ok: false,
1274
+ invalidReason: body.invalidReason ?? "verify_rejected"
994
1275
  };
995
- let retryResponse;
996
- try {
997
- retryResponse = await fetch(input, retryInit);
998
- } catch (err) {
999
- const message = err instanceof Error ? err.message : String(err);
1000
- logger5.error(`[x402] Retry request failed: ${message}`);
1001
- circuitBreaker.recordFailure();
1002
- return initialResponse;
1003
- }
1004
- const sessionId = retryResponse.headers.get("x-upto-session-id") ?? retryResponse.headers.get("X-PAYMENT-RESPONSE") ?? "";
1005
- const record = {
1006
- id: generateId(),
1007
- direction: "outgoing",
1008
- counterparty: requirement.payTo,
1009
- amount,
1010
- network: requirement.network,
1011
- txHash: sessionId,
1012
- resource: url,
1013
- status: retryResponse.ok ? "confirmed" : "failed",
1014
- createdAt: new Date().toISOString(),
1015
- metadata: {
1016
- scheme: requirement.scheme,
1017
- description: requirement.description
1276
+ } catch (e) {
1277
+ const msg = e instanceof Error ? e.message : String(e);
1278
+ return { ok: false, invalidReason: `verify_fetch_error:${msg}` };
1279
+ }
1280
+ }
1281
+ async function settlePaymentPayloadViaFacilitatorPost(runtime, paymentPayload, paymentRequirements) {
1282
+ const url = getFacilitatorSettlePostUrl(runtime);
1283
+ if (!url) {
1284
+ return { ok: false, invalidReason: "no_facilitator_settle_url" };
1285
+ }
1286
+ try {
1287
+ const res = await fetch(url, {
1288
+ method: "POST",
1289
+ headers: {
1290
+ Accept: "application/json",
1291
+ "Content-Type": "application/json",
1292
+ "User-Agent": "ElizaOS-X402-Agent/1.0"
1293
+ },
1294
+ body: JSON.stringify({ paymentPayload, paymentRequirements }),
1295
+ signal: AbortSignal.timeout(30000)
1296
+ });
1297
+ const text = await res.text();
1298
+ let body = {};
1299
+ if (text) {
1300
+ try {
1301
+ body = JSON.parse(text);
1302
+ } catch {
1303
+ return { ok: false, invalidReason: "invalid_settle_response_json" };
1018
1304
  }
1305
+ }
1306
+ if (!res.ok) {
1307
+ return {
1308
+ ok: false,
1309
+ invalidReason: typeof body.errorReason === "string" ? body.errorReason : typeof body.invalidReason === "string" ? body.invalidReason : `settle_http_${res.status}`
1310
+ };
1311
+ }
1312
+ const explicitFailure = body.success === false || body.isValid === false;
1313
+ const explicitSuccess = body.success === true || body.isValid === true;
1314
+ const success = !explicitFailure && explicitSuccess;
1315
+ if (!success) {
1316
+ return {
1317
+ ok: false,
1318
+ invalidReason: typeof body.errorReason === "string" ? body.errorReason : typeof body.invalidReason === "string" ? body.invalidReason : `settle_http_${res.status}`
1319
+ };
1320
+ }
1321
+ const paymentResponse = Buffer.from(JSON.stringify(body), "utf8").toString("base64");
1322
+ return {
1323
+ ok: true,
1324
+ paymentResponse,
1325
+ transaction: typeof body.transaction === "string" ? body.transaction : undefined,
1326
+ payer: typeof body.payer === "string" ? body.payer : undefined
1019
1327
  };
1020
- try {
1021
- await storage.recordPayment(record);
1022
- } catch (err) {
1023
- const message = err instanceof Error ? err.message : String(err);
1024
- logger5.error(`[x402] Failed to record payment: ${message}`);
1328
+ } catch (e) {
1329
+ const msg = e instanceof Error ? e.message : String(e);
1330
+ return { ok: false, invalidReason: `settle_fetch_error:${msg}` };
1331
+ }
1332
+ }
1333
+
1334
+ // src/x402-types.ts
1335
+ var VALID_NETWORKS = [
1336
+ "base-sepolia",
1337
+ "base",
1338
+ "avalanche-fuji",
1339
+ "avalanche",
1340
+ "iotex",
1341
+ "solana-devnet",
1342
+ "solana",
1343
+ "sei",
1344
+ "sei-testnet",
1345
+ "polygon",
1346
+ "polygon-amoy",
1347
+ "bsc",
1348
+ "bsc-testnet",
1349
+ "peaq"
1350
+ ];
1351
+ function isValidUrl(url) {
1352
+ try {
1353
+ const parsed = new URL(url);
1354
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
1355
+ } catch {
1356
+ return false;
1357
+ }
1358
+ }
1359
+ function isValidWalletAddress(address, network) {
1360
+ if (!address || typeof address !== "string")
1361
+ return false;
1362
+ if (network.includes("solana")) {
1363
+ return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
1364
+ }
1365
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
1366
+ }
1367
+ function validateAccepts(accepts) {
1368
+ const errors = [];
1369
+ if (accepts.scheme !== "exact") {
1370
+ errors.push('scheme must be "exact"');
1371
+ }
1372
+ if (!accepts.network || !VALID_NETWORKS.includes(accepts.network)) {
1373
+ errors.push(`network must be one of: ${VALID_NETWORKS.join(", ")}`);
1374
+ }
1375
+ if (!accepts.maxAmountRequired || typeof accepts.maxAmountRequired !== "string") {
1376
+ errors.push("maxAmountRequired is required and must be a string");
1377
+ }
1378
+ if (!accepts.resource || typeof accepts.resource !== "string") {
1379
+ errors.push("resource is required and must be a string (full URL)");
1380
+ } else if (!isValidUrl(accepts.resource)) {
1381
+ errors.push("resource must be a valid URL (must start with http:// or https://)");
1382
+ }
1383
+ if (!accepts.description || typeof accepts.description !== "string") {
1384
+ errors.push("description is required and must be a string");
1385
+ }
1386
+ if (!accepts.mimeType || typeof accepts.mimeType !== "string") {
1387
+ errors.push('mimeType is required and must be a string (e.g., "application/json")');
1388
+ }
1389
+ if (!accepts.payTo || typeof accepts.payTo !== "string") {
1390
+ errors.push("payTo is required and must be a string (wallet address)");
1391
+ } else if (accepts.network && !isValidWalletAddress(accepts.payTo, accepts.network)) {
1392
+ errors.push(`payTo must be a valid wallet address for network ${accepts.network}`);
1393
+ }
1394
+ if (!accepts.maxTimeoutSeconds || typeof accepts.maxTimeoutSeconds !== "number") {
1395
+ errors.push("maxTimeoutSeconds is required and must be a number");
1396
+ }
1397
+ if (!accepts.asset || typeof accepts.asset !== "string") {
1398
+ errors.push('asset is required and must be a string (e.g., "USDC", "ETH")');
1399
+ }
1400
+ if (accepts.outputSchema) {
1401
+ const schema = accepts.outputSchema;
1402
+ if (schema.input.type !== "http") {
1403
+ errors.push('outputSchema.input.type must be "http"');
1404
+ }
1405
+ if (!schema.input.method || !["GET", "POST"].includes(schema.input.method)) {
1406
+ errors.push('outputSchema.input.method must be "GET" or "POST"');
1407
+ }
1408
+ if (schema.input.bodyType) {
1409
+ const validBodyTypes = [
1410
+ "json",
1411
+ "form-data",
1412
+ "multipart-form-data",
1413
+ "text",
1414
+ "binary"
1415
+ ];
1416
+ if (!validBodyTypes.includes(schema.input.bodyType)) {
1417
+ errors.push(`outputSchema.input.bodyType must be one of: ${validBodyTypes.join(", ")}`);
1418
+ }
1025
1419
  }
1026
- if (retryResponse.ok) {
1027
- circuitBreaker.recordSuccess(amount);
1028
- logger5.info(`[x402] Payment successful: ${amount} to ${requirement.payTo}`);
1420
+ }
1421
+ return {
1422
+ valid: errors.length === 0,
1423
+ errors
1424
+ };
1425
+ }
1426
+ function validateX402Response(response) {
1427
+ const errors = [];
1428
+ if (typeof response.x402Version !== "number") {
1429
+ errors.push("x402Version is required and must be a number");
1430
+ }
1431
+ if (response.accepts) {
1432
+ if (!Array.isArray(response.accepts)) {
1433
+ errors.push("accepts must be an array");
1029
1434
  } else {
1030
- circuitBreaker.recordFailure();
1031
- logger5.warn(`[x402] Payment request returned ${retryResponse.status} after payment`);
1435
+ response.accepts.forEach((accepts, index) => {
1436
+ const validation = validateAccepts(accepts);
1437
+ if (!validation.valid) {
1438
+ errors.push(`accepts[${index}]: ${validation.errors.join(", ")}`);
1439
+ }
1440
+ });
1032
1441
  }
1033
- return retryResponse;
1442
+ }
1443
+ return {
1444
+ valid: errors.length === 0,
1445
+ errors
1034
1446
  };
1035
1447
  }
1448
+ function createAccepts(params) {
1449
+ const accepts = {
1450
+ scheme: "exact",
1451
+ network: params.network,
1452
+ maxAmountRequired: params.maxAmountRequired,
1453
+ resource: params.resource,
1454
+ description: params.description,
1455
+ mimeType: params.mimeType || "application/json",
1456
+ payTo: params.payTo,
1457
+ maxTimeoutSeconds: params.maxTimeoutSeconds || 300,
1458
+ asset: params.asset
1459
+ };
1460
+ if (params.outputSchema) {
1461
+ accepts.outputSchema = params.outputSchema;
1462
+ }
1463
+ if (params.extra) {
1464
+ accepts.extra = params.extra;
1465
+ }
1466
+ const validation = validateAccepts(accepts);
1467
+ if (!validation.valid) {
1468
+ throw new Error(`Invalid Accepts object: ${validation.errors.join(", ")}`);
1469
+ }
1470
+ return accepts;
1471
+ }
1472
+ function createX402Response(params) {
1473
+ const response = {
1474
+ x402Version: 1,
1475
+ ...params
1476
+ };
1477
+ const validation = validateX402Response(response);
1478
+ if (!validation.valid) {
1479
+ throw new Error(`Invalid X402Response: ${validation.errors.join(", ")}`);
1480
+ }
1481
+ return response;
1482
+ }
1036
1483
 
1037
- // policy/circuit-breaker.ts
1038
- var DEFAULT_CONFIG = {
1039
- maxPaymentsPerMinute: 50,
1040
- anomalyMultiplier: 10,
1041
- cooldownMs: 60000,
1042
- recentWindowSize: 20
1043
- };
1044
-
1045
- class CircuitBreaker {
1046
- state = "closed";
1047
- config;
1048
- recentTimestamps = [];
1049
- recentAmounts = [];
1050
- trippedAt = 0;
1051
- lastTripReason = "";
1052
- constructor(config) {
1053
- this.config = { ...DEFAULT_CONFIG, ...config };
1054
- }
1055
- check(amount) {
1056
- const now = Date.now();
1057
- if (this.state === "open") {
1058
- if (now - this.trippedAt >= this.config.cooldownMs) {
1059
- this.state = "half-open";
1060
- } else {
1061
- return {
1062
- allowed: false,
1063
- reason: `Circuit breaker is OPEN: ${this.lastTripReason}. Resets in ${Math.ceil((this.config.cooldownMs - (now - this.trippedAt)) / 1000)}s`
1064
- };
1484
+ // src/payment-wrapper.ts
1485
+ var X402_ROUTE_PAYMENT_WRAPPED = Symbol.for("elizaos.x402.routePaymentWrapped");
1486
+ function isRoutePaymentWrapped(route) {
1487
+ return typeof route === "object" && route !== null && Reflect.get(route, X402_ROUTE_PAYMENT_WRAPPED) === true;
1488
+ }
1489
+ var DEBUG = process.env.DEBUG_X402_PAYMENTS === "true";
1490
+ function formatLogArg(arg) {
1491
+ if (typeof arg === "string")
1492
+ return arg;
1493
+ if (typeof arg === "bigint")
1494
+ return arg.toString();
1495
+ try {
1496
+ return JSON.stringify(arg);
1497
+ } catch {
1498
+ return String(arg);
1499
+ }
1500
+ }
1501
+ function log(...args) {
1502
+ if (DEBUG)
1503
+ logger5.debug(args.map(formatLogArg).join(" "));
1504
+ }
1505
+ function logSection(title) {
1506
+ if (DEBUG) {
1507
+ logger5.debug(`[x402] ${title}`);
1508
+ }
1509
+ }
1510
+ function logError(...args) {
1511
+ logger5.error(args.map(formatLogArg).join(" "));
1512
+ }
1513
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = [
1514
+ { name: "from", type: "address" },
1515
+ { name: "to", type: "address" },
1516
+ { name: "value", type: "uint256" },
1517
+ { name: "validAfter", type: "uint256" },
1518
+ { name: "validBefore", type: "uint256" },
1519
+ { name: "nonce", type: "bytes32" }
1520
+ ];
1521
+ var RECEIVE_WITH_AUTHORIZATION_TYPES = [
1522
+ { name: "from", type: "address" },
1523
+ { name: "to", type: "address" },
1524
+ { name: "value", type: "uint256" },
1525
+ { name: "validAfter", type: "uint256" },
1526
+ { name: "validBefore", type: "uint256" },
1527
+ { name: "nonce", type: "bytes32" }
1528
+ ];
1529
+ function getViemChain(network) {
1530
+ switch (network.toUpperCase()) {
1531
+ case "BASE":
1532
+ return base;
1533
+ case "POLYGON":
1534
+ return polygon;
1535
+ case "BSC":
1536
+ return bsc;
1537
+ case "ETHEREUM":
1538
+ return mainnet;
1539
+ default:
1540
+ return base;
1541
+ }
1542
+ }
1543
+ function getRpcUrl(network, runtime) {
1544
+ const networkUpper = network.toUpperCase();
1545
+ const settingKey = `${networkUpper}_RPC_URL`;
1546
+ const customRpc = runtime.getSetting(settingKey);
1547
+ if (customRpc && typeof customRpc === "string") {
1548
+ return customRpc;
1549
+ }
1550
+ switch (networkUpper) {
1551
+ case "BASE":
1552
+ return "https://mainnet.base.org";
1553
+ case "POLYGON":
1554
+ return "https://polygon-rpc.com";
1555
+ case "BSC":
1556
+ return "https://bsc-dataseed.binance.org";
1557
+ case "ETHEREUM":
1558
+ return "https://eth.llamarpc.com";
1559
+ default:
1560
+ return "https://mainnet.base.org";
1561
+ }
1562
+ }
1563
+ function getUsdcContractAddress(network) {
1564
+ switch (network.toUpperCase()) {
1565
+ case "BASE":
1566
+ return "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1567
+ case "POLYGON":
1568
+ return "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
1569
+ case "BSC":
1570
+ return "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
1571
+ case "ETHEREUM":
1572
+ return "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
1573
+ default:
1574
+ return "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1575
+ }
1576
+ }
1577
+ function chainIdToNetwork(chainId) {
1578
+ if (chainId === 8453)
1579
+ return "BASE";
1580
+ if (chainId === 137)
1581
+ return "POLYGON";
1582
+ if (chainId === 56)
1583
+ return "BSC";
1584
+ return null;
1585
+ }
1586
+ function sumOwnerMint(balances, owner, mint) {
1587
+ if (!balances?.length)
1588
+ return 0n;
1589
+ let s = 0n;
1590
+ for (const b of balances) {
1591
+ if (b.mint === mint && b.owner === owner) {
1592
+ s += BigInt(b.uiTokenAmount?.amount ?? "0");
1593
+ }
1594
+ }
1595
+ return s;
1596
+ }
1597
+ async function verifyPayment(params) {
1598
+ const {
1599
+ paymentProof,
1600
+ paymentId,
1601
+ route,
1602
+ priceInCents,
1603
+ paymentConfigNames,
1604
+ agentId,
1605
+ runtime,
1606
+ req
1607
+ } = params;
1608
+ logSection("PAYMENT VERIFICATION");
1609
+ log("Route:", route, "priceInCents:", priceInCents, "configs:", paymentConfigNames);
1610
+ if (!paymentProof && !paymentId) {
1611
+ logError("✗ No payment credentials provided");
1612
+ return { ok: false };
1613
+ }
1614
+ const replayKeys = collectReplayKeysToCheck(paymentProof, paymentId);
1615
+ if (!await replayGuardTryBegin(replayKeys, runtime, agentId)) {
1616
+ logError("✗ Payment credential in use or already consumed (replay protection)");
1617
+ return { ok: false };
1618
+ }
1619
+ let committed = false;
1620
+ const finishVerified = async (details) => {
1621
+ committed = true;
1622
+ await replayGuardCommit(replayKeys, runtime, agentId);
1623
+ return { ok: true, details };
1624
+ };
1625
+ try {
1626
+ const configsOrdered = paymentConfigNames.map((n) => ({
1627
+ name: n,
1628
+ cfg: getPaymentConfig(n, agentId)
1629
+ }));
1630
+ if (paymentProof) {
1631
+ try {
1632
+ const standardDecoded = decodeXPaymentHeader(typeof paymentProof === "string" ? paymentProof : "");
1633
+ if (isX402StandardPaymentPayload(standardDecoded)) {
1634
+ const match = findMatchingPaymentConfigForStandardPayload(standardDecoded, paymentConfigNames, priceInCents, agentId);
1635
+ if (!match) {
1636
+ log("Standard X-Payment payload did not match any allowed payment config");
1637
+ return { ok: false };
1638
+ }
1639
+ const paymentRequirements = buildFacilitatorPaymentRequirements({
1640
+ routePath: route,
1641
+ priceInCents,
1642
+ configName: match.name,
1643
+ agentId
1644
+ });
1645
+ const postResult = await verifyPaymentPayloadViaFacilitatorPost(runtime, standardDecoded, paymentRequirements);
1646
+ if (postResult.ok !== true) {
1647
+ log("Standard X-Payment facilitator verify failed:", postResult.invalidReason);
1648
+ return { ok: false };
1649
+ }
1650
+ const settleResult = await settlePaymentPayloadViaFacilitatorPost(runtime, standardDecoded, paymentRequirements);
1651
+ if (settleResult.ok === false) {
1652
+ log("Standard X-Payment facilitator settle failed:", settleResult.invalidReason);
1653
+ return { ok: false };
1654
+ }
1655
+ log("✓ Standard X-Payment verified and settled via facilitator", match.name);
1656
+ return await finishVerified({
1657
+ paymentConfig: match.name,
1658
+ network: match.cfg.network,
1659
+ amountAtomic: paymentRequirements.amount,
1660
+ symbol: match.cfg.symbol,
1661
+ payer: settleResult.payer ?? postResult.payer ?? standardDecoded.payload.authorization.from,
1662
+ proofId: standardDecoded.payload.signature,
1663
+ paymentResponse: settleResult.paymentResponse
1664
+ });
1665
+ }
1666
+ const decodedProof = decodePaymentProofForParsing(paymentProof);
1667
+ try {
1668
+ const jsonProof = JSON.parse(decodedProof);
1669
+ log("Detected JSON payment proof");
1670
+ const authData = jsonProof.payload ? {
1671
+ signature: jsonProof.payload.signature,
1672
+ authorization: jsonProof.payload.authorization,
1673
+ network: jsonProof.network,
1674
+ scheme: jsonProof.scheme,
1675
+ domain: jsonProof.payload.domain ?? jsonProof.domain
1676
+ } : { ...jsonProof, domain: jsonProof.domain };
1677
+ const domain = authData.domain ?? jsonProof.domain;
1678
+ const chainId = domain?.chainId;
1679
+ const inferredNet = typeof chainId === "number" ? chainIdToNetwork(chainId) : null;
1680
+ const authObj = authData;
1681
+ const hasEip712 = typeof authObj.signature === "string" && authObj.authorization && typeof authObj.authorization === "object";
1682
+ if (hasEip712) {
1683
+ const evmCandidates = configsOrdered.filter((c) => c.cfg.network === "BASE" || c.cfg.network === "POLYGON" || c.cfg.network === "BSC");
1684
+ for (const { name, cfg } of evmCandidates) {
1685
+ if (inferredNet && cfg.network !== inferredNet)
1686
+ continue;
1687
+ if (domain?.verifyingContract && domain.verifyingContract.toLowerCase() !== cfg.assetReference.toLowerCase()) {
1688
+ continue;
1689
+ }
1690
+ const atomic = atomicAmountForPriceInCents(priceInCents, cfg);
1691
+ const recipient = cfg.paymentAddress;
1692
+ const ok = await verifyEvmPayment(JSON.stringify(authData), recipient, atomic, cfg.network, runtime, req, {
1693
+ eip712TokenContract: cfg.assetReference,
1694
+ erc20Contract: cfg.assetReference
1695
+ });
1696
+ if (ok) {
1697
+ const auth = authObj.authorization;
1698
+ log(`✓ ${cfg.network} payment verified (EIP-712) config=${name}`);
1699
+ return await finishVerified({
1700
+ paymentConfig: name,
1701
+ network: cfg.network,
1702
+ amountAtomic: atomic,
1703
+ symbol: cfg.symbol,
1704
+ payer: auth?.from,
1705
+ proofId: typeof authObj.signature === "string" ? authObj.signature : undefined
1706
+ });
1707
+ }
1708
+ }
1709
+ }
1710
+ } catch {
1711
+ const parts = decodedProof.split(":");
1712
+ if (parts.length >= 3) {
1713
+ const [networkRaw, address, signature] = parts;
1714
+ const network = networkRaw.toUpperCase();
1715
+ log(`Legacy format: ${network}`);
1716
+ if (network === "SOLANA") {
1717
+ for (const { name, cfg } of configsOrdered) {
1718
+ if (cfg.network !== "SOLANA")
1719
+ continue;
1720
+ if (address.trim() !== cfg.paymentAddress.trim()) {
1721
+ logError("Solana legacy proof: recipient field must equal the route pay-to address (expected", cfg.paymentAddress, "got", address);
1722
+ continue;
1723
+ }
1724
+ const atomic = atomicAmountForPriceInCents(priceInCents, cfg);
1725
+ if (await verifySolanaPayment(signature, cfg.paymentAddress, cfg.assetReference, atomic, runtime)) {
1726
+ log("✓ Solana payment verified");
1727
+ return await finishVerified({
1728
+ paymentConfig: name,
1729
+ network: "SOLANA",
1730
+ amountAtomic: atomic,
1731
+ symbol: cfg.symbol,
1732
+ proofId: signature
1733
+ });
1734
+ }
1735
+ }
1736
+ } else if (network === "BASE" || network === "POLYGON" || network === "BSC") {
1737
+ for (const { name, cfg } of configsOrdered) {
1738
+ if (cfg.network !== network)
1739
+ continue;
1740
+ if (cfg.assetNamespace !== "erc20")
1741
+ continue;
1742
+ const atomic = atomicAmountForPriceInCents(priceInCents, cfg);
1743
+ if (await verifyEvmPayment(signature, cfg.paymentAddress, atomic, network, runtime, req, {
1744
+ erc20Contract: cfg.assetReference,
1745
+ eip712TokenContract: cfg.assetReference
1746
+ })) {
1747
+ log(`✓ ${network} payment verified`);
1748
+ return await finishVerified({
1749
+ paymentConfig: name,
1750
+ network: cfg.network,
1751
+ amountAtomic: atomic,
1752
+ symbol: cfg.symbol,
1753
+ proofId: signature
1754
+ });
1755
+ }
1756
+ }
1757
+ }
1758
+ } else if (parts.length === 1 && parts[0].length > 50) {
1759
+ const sigOnly = parts[0];
1760
+ for (const { name, cfg } of configsOrdered) {
1761
+ if (cfg.network !== "SOLANA")
1762
+ continue;
1763
+ const atomic = atomicAmountForPriceInCents(priceInCents, cfg);
1764
+ if (await verifySolanaPayment(sigOnly, cfg.paymentAddress, cfg.assetReference, atomic, runtime)) {
1765
+ log("✓ Solana payment verified (raw signature)");
1766
+ return await finishVerified({
1767
+ paymentConfig: name,
1768
+ network: "SOLANA",
1769
+ amountAtomic: atomic,
1770
+ symbol: cfg.symbol,
1771
+ proofId: sigOnly
1772
+ });
1773
+ }
1774
+ }
1775
+ }
1776
+ }
1777
+ } catch (error) {
1778
+ logError("Blockchain verification error:", error instanceof Error ? error.message : String(error));
1065
1779
  }
1066
1780
  }
1067
- const oneMinuteAgo = now - 60000;
1068
- this.recentTimestamps = this.recentTimestamps.filter((t) => t > oneMinuteAgo);
1069
- if (this.recentTimestamps.length >= this.config.maxPaymentsPerMinute) {
1070
- this.trip(`Rate exceeded: ${this.recentTimestamps.length} payments in the last minute`);
1071
- return { allowed: false, reason: this.lastTripReason };
1072
- }
1073
- if (this.recentAmounts.length >= 3) {
1074
- const sum = this.recentAmounts.reduce((a, b) => a + b, 0n);
1075
- const avg = sum / BigInt(this.recentAmounts.length);
1076
- if (avg > 0n && amount > avg * BigInt(this.config.anomalyMultiplier)) {
1077
- this.trip(`Anomaly detected: payment of ${amount} is >${this.config.anomalyMultiplier}x the average of ${avg}`);
1078
- return { allowed: false, reason: this.lastTripReason };
1781
+ if (paymentId) {
1782
+ try {
1783
+ if (await verifyPaymentIdViaFacilitator(paymentId, runtime, {
1784
+ resource: toResourceUrl(route),
1785
+ routePath: route,
1786
+ priceInCents,
1787
+ paymentConfigNames
1788
+ })) {
1789
+ log("✓ Facilitator payment verified");
1790
+ return await finishVerified({
1791
+ paymentConfig: "facilitator",
1792
+ network: "facilitator",
1793
+ amountAtomic: "",
1794
+ proofId: paymentId
1795
+ });
1796
+ }
1797
+ } catch (error) {
1798
+ logError("Facilitator verification error:", error instanceof Error ? error.message : String(error));
1079
1799
  }
1080
1800
  }
1081
- return { allowed: true, reason: "" };
1801
+ logError("✗ All payment verification strategies failed");
1802
+ return { ok: false };
1803
+ } finally {
1804
+ if (!committed)
1805
+ await replayGuardAbortAsync(replayKeys, runtime, agentId);
1082
1806
  }
1083
- recordSuccess(amount) {
1084
- const now = Date.now();
1085
- this.recentTimestamps.push(now);
1086
- this.recentAmounts.push(amount);
1087
- if (this.recentAmounts.length > this.config.recentWindowSize) {
1088
- this.recentAmounts.shift();
1089
- }
1090
- if (this.state === "half-open") {
1091
- this.state = "closed";
1092
- this.lastTripReason = "";
1807
+ }
1808
+ function sanitizePaymentId(paymentId) {
1809
+ const cleaned = paymentId.trim();
1810
+ if (!/^[a-zA-Z0-9_-]+$/.test(cleaned)) {
1811
+ throw new Error("Invalid payment ID format");
1812
+ }
1813
+ if (cleaned.length > 128) {
1814
+ throw new Error("Payment ID too long");
1815
+ }
1816
+ return cleaned;
1817
+ }
1818
+ async function verifyPaymentIdViaFacilitator(paymentId, runtime, ctx) {
1819
+ logSection("FACILITATOR VERIFICATION");
1820
+ let cleanPaymentId;
1821
+ try {
1822
+ cleanPaymentId = sanitizePaymentId(paymentId);
1823
+ log("Payment ID:", cleanPaymentId);
1824
+ } catch (error) {
1825
+ logError("Invalid payment ID:", error instanceof Error ? error.message : String(error));
1826
+ return false;
1827
+ }
1828
+ const facilitatorUrlSetting = runtime.getSetting("X402_FACILITATOR_URL");
1829
+ const facilitatorUrl = typeof facilitatorUrlSetting === "string" ? facilitatorUrlSetting : "https://x402.elizacloud.ai/api/facilitator";
1830
+ if (!facilitatorUrl) {
1831
+ logError("⚠️ No facilitator URL configured");
1832
+ return false;
1833
+ }
1834
+ try {
1835
+ const cleanUrl = facilitatorUrl.replace(/\/$/, "");
1836
+ const verifyPath = `${cleanUrl}/verify/${encodeURIComponent(cleanPaymentId)}`;
1837
+ const url = new URL(verifyPath);
1838
+ if (ctx) {
1839
+ url.searchParams.set("resource", ctx.resource);
1840
+ url.searchParams.set("routePath", ctx.routePath);
1841
+ url.searchParams.set("priceInCents", String(ctx.priceInCents));
1842
+ url.searchParams.set("paymentConfigs", ctx.paymentConfigNames.join(","));
1843
+ }
1844
+ const endpoint = url.toString();
1845
+ log("Verifying at:", endpoint);
1846
+ const response = await fetch(endpoint, {
1847
+ method: "GET",
1848
+ headers: {
1849
+ Accept: "application/json",
1850
+ "User-Agent": "ElizaOS-X402-Client/1.0"
1851
+ },
1852
+ signal: AbortSignal.timeout(1e4)
1853
+ });
1854
+ const responseText = await response.text();
1855
+ const responseData = responseText ? JSON.parse(responseText) : {};
1856
+ if (response.ok) {
1857
+ const isValid = responseData?.valid !== false && responseData?.verified !== false;
1858
+ if (isValid) {
1859
+ if (ctx && !facilitatorVerifyResponseMatchesRoute(responseData, ctx, isFacilitatorBindingRelaxed())) {
1860
+ logError(isFacilitatorBindingRelaxed() ? "✗ Facilitator response failed route binding checks" : "✗ Facilitator strict binding failed (response must include matching resource, routePath or route, priceInCents, paymentConfig). Set X402_FACILITATOR_RELAXED_BINDING=1 if your facilitator cannot echo these fields yet.");
1861
+ return false;
1862
+ }
1863
+ log("✓ Facilitator verified payment");
1864
+ return true;
1865
+ } else {
1866
+ logError("✗ Payment invalid per facilitator");
1867
+ return false;
1868
+ }
1869
+ } else if (response.status === 404) {
1870
+ logError("✗ Payment ID not found (404)");
1871
+ return false;
1872
+ } else if (response.status === 410) {
1873
+ logError("✗ Payment ID already used (410 - replay attack prevented)");
1874
+ return false;
1875
+ } else {
1876
+ logError(`✗ Facilitator error: ${response.status} ${response.statusText}`);
1877
+ return false;
1093
1878
  }
1094
- }
1095
- recordFailure() {
1096
- if (this.state === "half-open") {
1097
- this.trip("Probe payment failed in half-open state");
1879
+ } catch (error) {
1880
+ if (error instanceof Error && error.name === "AbortError") {
1881
+ logError("✗ Facilitator request timed out (10s)");
1882
+ } else {
1883
+ logError("✗ Facilitator verification error:", error instanceof Error ? error.message : String(error));
1098
1884
  }
1099
- }
1100
- getState() {
1101
- return this.state;
1102
- }
1103
- getTripReason() {
1104
- return this.lastTripReason;
1105
- }
1106
- reset() {
1107
- this.state = "closed";
1108
- this.lastTripReason = "";
1109
- this.trippedAt = 0;
1110
- }
1111
- trip(reason) {
1112
- this.state = "open";
1113
- this.trippedAt = Date.now();
1114
- this.lastTripReason = reason;
1885
+ return false;
1115
1886
  }
1116
1887
  }
1117
-
1118
- // policy/engine.ts
1119
- var ALLOW = { allowed: true, reason: "" };
1120
- function deny(reason) {
1121
- return { allowed: false, reason };
1888
+ function sanitizeSolanaSignature(signature) {
1889
+ const cleaned = signature.trim();
1890
+ if (!/^[1-9A-HJ-NP-Za-km-z]{87,88}$/.test(cleaned)) {
1891
+ throw new Error("Invalid Solana signature format");
1892
+ }
1893
+ return cleaned;
1122
1894
  }
1123
-
1124
- class PolicyEngine {
1125
- policy;
1126
- storage;
1127
- constructor(policy, storage) {
1128
- this.policy = policy;
1129
- this.storage = storage;
1895
+ async function verifySolanaPayment(signature, expectedRecipient, expectedMint, expectedAmountAtomic, runtime) {
1896
+ let cleanSignature;
1897
+ try {
1898
+ cleanSignature = sanitizeSolanaSignature(signature);
1899
+ log("Verifying Solana transaction:", `${cleanSignature.substring(0, 20)}...`);
1900
+ } catch (error) {
1901
+ logError("Invalid signature:", error instanceof Error ? error.message : String(error));
1902
+ return false;
1130
1903
  }
1131
- updatePolicy(partial) {
1132
- if (partial.outgoing) {
1133
- this.policy.outgoing = { ...this.policy.outgoing, ...partial.outgoing };
1904
+ try {
1905
+ const { Connection } = await import("@solana/web3.js");
1906
+ const rpcUrlSetting = runtime.getSetting("SOLANA_RPC_URL");
1907
+ const rpcUrl = typeof rpcUrlSetting === "string" ? rpcUrlSetting : "https://api.mainnet-beta.solana.com";
1908
+ const connection = new Connection(rpcUrl);
1909
+ const tx = await connection.getTransaction(cleanSignature, {
1910
+ maxSupportedTransactionVersion: 0
1911
+ });
1912
+ if (!tx) {
1913
+ logError("Transaction not found on Solana blockchain");
1914
+ return false;
1915
+ }
1916
+ if (tx.meta?.err) {
1917
+ logError("Transaction failed on-chain:", tx.meta.err);
1918
+ return false;
1134
1919
  }
1135
- if (partial.incoming) {
1136
- this.policy.incoming = { ...this.policy.incoming, ...partial.incoming };
1920
+ const meta = tx.meta;
1921
+ const pre = sumOwnerMint(meta?.preTokenBalances, expectedRecipient, expectedMint);
1922
+ const post = sumOwnerMint(meta?.postTokenBalances, expectedRecipient, expectedMint);
1923
+ const delta = post - pre;
1924
+ const need = BigInt(expectedAmountAtomic);
1925
+ if (delta < need) {
1926
+ logError("Solana SPL credit too low:", delta.toString(), "vs required", need.toString());
1927
+ return false;
1137
1928
  }
1929
+ log("✓ Solana SPL transfer verified");
1930
+ return true;
1931
+ } catch (error) {
1932
+ logError("Solana verification error:", error instanceof Error ? error.message : String(error));
1933
+ return false;
1138
1934
  }
1139
- getPolicy() {
1140
- return {
1141
- outgoing: { ...this.policy.outgoing },
1142
- incoming: { ...this.policy.incoming }
1143
- };
1935
+ }
1936
+ function sanitizePaymentProof(paymentData) {
1937
+ const cleaned = paymentData.trim();
1938
+ if (cleaned.length > 1e4) {
1939
+ throw new Error("Payment proof too large");
1144
1940
  }
1145
- async evaluateOutgoing(request) {
1146
- const limits = this.policy.outgoing;
1147
- if (request.amount > limits.maxPerTransaction) {
1148
- return deny(`Amount ${request.amount} exceeds per-transaction limit of ${limits.maxPerTransaction}`);
1149
- }
1150
- if (limits.blockedRecipients.length > 0) {
1151
- const normalized = request.recipient.toLowerCase();
1152
- if (limits.blockedRecipients.some((addr) => addr.toLowerCase() === normalized)) {
1153
- return deny(`Recipient ${request.recipient} is blocked`);
1154
- }
1155
- }
1156
- if (limits.allowedRecipients.length > 0) {
1157
- const normalized = request.recipient.toLowerCase();
1158
- if (!limits.allowedRecipients.some((addr) => addr.toLowerCase() === normalized)) {
1159
- return deny(`Recipient ${request.recipient} is not in the allow list`);
1160
- }
1161
- }
1162
- const currentTotal = await this.storage.getTotal("outgoing", limits.windowMs);
1163
- if (currentTotal + request.amount > limits.maxTotal) {
1164
- return deny(`Total spend would be ${currentTotal + request.amount}, exceeding window limit of ${limits.maxTotal}`);
1165
- }
1166
- const currentCount = await this.storage.getCount("outgoing", limits.windowMs);
1167
- if (currentCount >= limits.maxTransactions) {
1168
- return deny(`Transaction count ${currentCount} has reached the limit of ${limits.maxTransactions}`);
1169
- }
1170
- return ALLOW;
1941
+ return cleaned;
1942
+ }
1943
+ async function verifyEvmPayment(paymentData, expectedRecipient, expectedAmountAtomic, network, runtime, req, opts) {
1944
+ let cleanPaymentData;
1945
+ try {
1946
+ cleanPaymentData = sanitizePaymentProof(paymentData);
1947
+ log(`Verifying ${network} payment:`, `${cleanPaymentData.substring(0, 20)}...`);
1948
+ } catch (error) {
1949
+ logError("Invalid payment data:", error instanceof Error ? error.message : String(error));
1950
+ return false;
1171
1951
  }
1172
- async evaluateIncoming(request) {
1173
- const limits = this.policy.incoming;
1174
- if (request.amount < limits.minPerTransaction) {
1175
- return deny(`Amount ${request.amount} is below minimum of ${limits.minPerTransaction}`);
1176
- }
1177
- if (limits.blockedSenders.length > 0) {
1178
- const normalized = request.sender.toLowerCase();
1179
- if (limits.blockedSenders.some((addr) => addr.toLowerCase() === normalized)) {
1180
- return deny(`Sender ${request.sender} is blocked`);
1181
- }
1952
+ try {
1953
+ if (cleanPaymentData.match(/^0x[a-fA-F0-9]{64}$/)) {
1954
+ log("Detected transaction hash format");
1955
+ return await verifyEvmTransaction(cleanPaymentData, expectedRecipient, expectedAmountAtomic, network, runtime, opts?.erc20Contract);
1182
1956
  }
1183
- if (limits.allowedSenders.length > 0) {
1184
- const normalized = request.sender.toLowerCase();
1185
- if (!limits.allowedSenders.some((addr) => addr.toLowerCase() === normalized)) {
1186
- return deny(`Sender ${request.sender} is not in the allow list`);
1957
+ try {
1958
+ const parsed = JSON.parse(cleanPaymentData);
1959
+ if (typeof parsed === "object" && parsed !== null) {
1960
+ const proof = parsed;
1961
+ if (proof.signature || proof.v && proof.r && proof.s) {
1962
+ log("Detected EIP-712 signature format");
1963
+ const allowEip712 = process.env.X402_ALLOW_EIP712_SIGNATURE_VERIFICATION === "true" || process.env.X402_ALLOW_EIP712_SIGNATURE_VERIFICATION === "1";
1964
+ if (!allowEip712) {
1965
+ logError("EIP-712 authorization proofs are disabled (they do not prove on-chain settlement). Set X402_ALLOW_EIP712_SIGNATURE_VERIFICATION=1 only if you accept that risk.");
1966
+ return false;
1967
+ }
1968
+ const token = opts?.eip712TokenContract;
1969
+ if (!token) {
1970
+ logError("EIP-712 verification missing expected token contract");
1971
+ return false;
1972
+ }
1973
+ return await verifyEip712Authorization(parsed, expectedRecipient, expectedAmountAtomic, token, network, runtime, req);
1974
+ }
1187
1975
  }
1976
+ } catch {}
1977
+ if (cleanPaymentData.match(/^0x[a-fA-F0-9]{130}$/)) {
1978
+ logError("Raw signature detected but authorization parameters missing");
1979
+ return false;
1188
1980
  }
1189
- return ALLOW;
1981
+ logError("Unrecognized EVM payment format");
1982
+ return false;
1983
+ } catch (error) {
1984
+ logError("EVM verification error:", error instanceof Error ? error.message : String(error));
1985
+ return false;
1190
1986
  }
1191
1987
  }
1192
-
1193
- // storage/memory.ts
1194
- class MemoryPaymentStorage {
1195
- records = [];
1196
- async recordPayment(record) {
1197
- this.records.push({
1198
- ...record,
1199
- metadata: { ...record.metadata }
1988
+ async function verifyEvmTransaction(txHash, expectedRecipient, expectedAmountAtomic, network, runtime, tokenContract) {
1989
+ log("Verifying on-chain transaction:", txHash);
1990
+ try {
1991
+ const rpcUrl = getRpcUrl(network, runtime);
1992
+ const chain = getViemChain(network);
1993
+ const { createPublicClient, http, decodeFunctionData, parseAbi } = await import("viem");
1994
+ const publicClient = createPublicClient({
1995
+ chain,
1996
+ transport: http(rpcUrl)
1200
1997
  });
1201
- }
1202
- async getTotal(direction, windowMs, scope) {
1203
- const cutoff = windowMs ? new Date(Date.now() - windowMs).toISOString() : undefined;
1204
- let total = 0n;
1205
- for (const r of this.records) {
1206
- if (r.direction !== direction)
1207
- continue;
1208
- if (cutoff && r.createdAt < cutoff)
1209
- continue;
1210
- if (scope && r.counterparty !== scope)
1211
- continue;
1212
- if (r.status === "failed" || r.status === "refunded")
1213
- continue;
1214
- total += r.amount;
1215
- }
1216
- return total;
1217
- }
1218
- async getRecords(filters) {
1219
- let result = [...this.records];
1220
- if (filters) {
1221
- if (filters.direction) {
1222
- result = result.filter((r) => r.direction === filters.direction);
1223
- }
1224
- if (filters.counterparty) {
1225
- result = result.filter((r) => r.counterparty.toLowerCase() === filters.counterparty.toLowerCase());
1226
- }
1227
- if (filters.status) {
1228
- result = result.filter((r) => r.status === filters.status);
1229
- }
1230
- if (filters.network) {
1231
- result = result.filter((r) => r.network === filters.network);
1998
+ const receipt = await publicClient.getTransactionReceipt({
1999
+ hash: txHash
2000
+ });
2001
+ if (receipt.status !== "success") {
2002
+ logError("Transaction failed on-chain");
2003
+ return false;
2004
+ }
2005
+ const tx = await publicClient.getTransaction({ hash: txHash });
2006
+ const targetContract = tokenContract ?? getUsdcContractAddress(network);
2007
+ const expectedUnits = BigInt(expectedAmountAtomic);
2008
+ if (receipt.to?.toLowerCase() !== targetContract.toLowerCase()) {
2009
+ logError("Transaction not to expected token contract:", receipt.to);
2010
+ return false;
2011
+ }
2012
+ log("Detected ERC-20 token transfer");
2013
+ if (tx.input === "0x") {
2014
+ logError("No input data in transaction");
2015
+ return false;
2016
+ }
2017
+ try {
2018
+ const erc20Abi = parseAbi([
2019
+ "function transfer(address to, uint256 amount) returns (bool)",
2020
+ "function transferFrom(address from, address to, uint256 amount) returns (bool)"
2021
+ ]);
2022
+ const decoded = decodeFunctionData({
2023
+ abi: erc20Abi,
2024
+ data: tx.input
2025
+ });
2026
+ const functionName = decoded.functionName;
2027
+ log("Decoded function:", functionName);
2028
+ let transferTo;
2029
+ let transferAmount;
2030
+ if (functionName === "transfer") {
2031
+ const [to, amount] = decoded.args;
2032
+ transferTo = to;
2033
+ transferAmount = amount;
2034
+ } else if (functionName === "transferFrom") {
2035
+ const [_from, to, amount] = decoded.args;
2036
+ transferTo = to;
2037
+ transferAmount = amount;
2038
+ } else {
2039
+ logError("Unknown ERC-20 function:", functionName);
2040
+ return false;
1232
2041
  }
1233
- if (filters.since) {
1234
- result = result.filter((r) => r.createdAt >= filters.since);
2042
+ log("Transfer to:", transferTo, "Amount:", transferAmount.toString());
2043
+ if (transferTo.toLowerCase() !== expectedRecipient.toLowerCase()) {
2044
+ logError("ERC-20 transfer recipient mismatch:", transferTo, "vs", expectedRecipient);
2045
+ return false;
1235
2046
  }
1236
- if (filters.until) {
1237
- result = result.filter((r) => r.createdAt <= filters.until);
2047
+ if (transferAmount < expectedUnits) {
2048
+ logError("ERC-20 transfer amount too low:", transferAmount.toString(), "vs", expectedUnits.toString());
2049
+ return false;
1238
2050
  }
2051
+ log("✓ ERC-20 transaction verified");
2052
+ return true;
2053
+ } catch (decodeError) {
2054
+ logError("Failed to decode ERC-20 transfer:", decodeError instanceof Error ? decodeError.message : String(decodeError));
2055
+ return false;
1239
2056
  }
1240
- result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1241
- const offset = filters?.offset ?? 0;
1242
- const limit = filters?.limit ?? result.length;
1243
- return result.slice(offset, offset + limit);
1244
- }
1245
- async getCount(direction, windowMs) {
1246
- const cutoff = windowMs ? new Date(Date.now() - windowMs).toISOString() : undefined;
1247
- let count = 0;
1248
- for (const r of this.records) {
1249
- if (r.direction !== direction)
1250
- continue;
1251
- if (cutoff && r.createdAt < cutoff)
1252
- continue;
1253
- if (r.status === "failed" || r.status === "refunded")
1254
- continue;
1255
- count++;
1256
- }
1257
- return count;
1258
- }
1259
- async clear() {
1260
- this.records = [];
2057
+ } catch (error) {
2058
+ logError("Transaction verification error:", error instanceof Error ? error.message : String(error));
2059
+ return false;
1261
2060
  }
1262
2061
  }
1263
-
1264
- // storage/sqlite.ts
1265
- import Database from "better-sqlite3";
1266
-
1267
- class SqlitePaymentStorage {
1268
- db;
1269
- constructor(dbPath) {
1270
- this.db = new Database(dbPath);
1271
- this.db.pragma("journal_mode = WAL");
1272
- this.db.pragma("foreign_keys = ON");
1273
- this.initialize();
1274
- }
1275
- initialize() {
1276
- this.db.exec(`
1277
- CREATE TABLE IF NOT EXISTS x402_payments (
1278
- id TEXT PRIMARY KEY,
1279
- direction TEXT NOT NULL CHECK(direction IN ('outgoing', 'incoming')),
1280
- counterparty TEXT NOT NULL,
1281
- amount TEXT NOT NULL,
1282
- network TEXT NOT NULL,
1283
- tx_hash TEXT NOT NULL DEFAULT '',
1284
- resource TEXT NOT NULL DEFAULT '',
1285
- status TEXT NOT NULL DEFAULT 'pending',
1286
- created_at TEXT NOT NULL,
1287
- metadata TEXT NOT NULL DEFAULT '{}'
1288
- );
1289
-
1290
- CREATE INDEX IF NOT EXISTS idx_x402_direction ON x402_payments(direction);
1291
- CREATE INDEX IF NOT EXISTS idx_x402_created_at ON x402_payments(created_at);
1292
- CREATE INDEX IF NOT EXISTS idx_x402_counterparty ON x402_payments(counterparty);
1293
- CREATE INDEX IF NOT EXISTS idx_x402_status ON x402_payments(status);
1294
- `);
1295
- }
1296
- async recordPayment(record) {
1297
- const stmt = this.db.prepare(`
1298
- INSERT INTO x402_payments (id, direction, counterparty, amount, network, tx_hash, resource, status, created_at, metadata)
1299
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1300
- `);
1301
- stmt.run(record.id, record.direction, record.counterparty, record.amount.toString(), record.network, record.txHash, record.resource, record.status, record.createdAt, JSON.stringify(record.metadata));
1302
- }
1303
- async getTotal(direction, windowMs, scope) {
1304
- let sql = "SELECT amount FROM x402_payments WHERE direction = ? AND status NOT IN ('failed', 'refunded')";
1305
- const params = [direction];
1306
- if (windowMs !== undefined) {
1307
- const cutoff = new Date(Date.now() - windowMs).toISOString();
1308
- sql += " AND created_at >= ?";
1309
- params.push(cutoff);
1310
- }
1311
- if (scope) {
1312
- sql += " AND counterparty = ?";
1313
- params.push(scope);
1314
- }
1315
- const rows = this.db.prepare(sql).all(...params);
1316
- let total = 0n;
1317
- for (const row of rows) {
1318
- total += BigInt(row.amount);
1319
- }
1320
- return total;
1321
- }
1322
- async getRecords(filters) {
1323
- let sql = "SELECT * FROM x402_payments WHERE 1=1";
1324
- const params = [];
1325
- if (filters) {
1326
- if (filters.direction) {
1327
- sql += " AND direction = ?";
1328
- params.push(filters.direction);
1329
- }
1330
- if (filters.counterparty) {
1331
- sql += " AND LOWER(counterparty) = LOWER(?)";
1332
- params.push(filters.counterparty);
1333
- }
1334
- if (filters.status) {
1335
- sql += " AND status = ?";
1336
- params.push(filters.status);
1337
- }
1338
- if (filters.network) {
1339
- sql += " AND network = ?";
1340
- params.push(filters.network);
1341
- }
1342
- if (filters.since) {
1343
- sql += " AND created_at >= ?";
1344
- params.push(filters.since);
1345
- }
1346
- if (filters.until) {
1347
- sql += " AND created_at <= ?";
1348
- params.push(filters.until);
1349
- }
2062
+ async function verifyEip712Authorization(paymentData, expectedRecipient, expectedAmountAtomic, expectedVerifyingContract, network, runtime, req) {
2063
+ log("Verifying EIP-712 authorization signature");
2064
+ if (typeof paymentData !== "object" || paymentData === null) {
2065
+ logError("Invalid payment data: must be an object");
2066
+ return false;
2067
+ }
2068
+ const proofData = paymentData;
2069
+ log("Payment data:", JSON.stringify(proofData, null, 2));
2070
+ try {
2071
+ let signature;
2072
+ let authorization;
2073
+ if (proofData.signature && typeof proofData.signature === "string") {
2074
+ signature = proofData.signature;
2075
+ authorization = proofData.authorization;
2076
+ } else if (proofData.v && proofData.r && proofData.s) {
2077
+ signature = `0x${proofData.r}${proofData.s}${proofData.v.toString(16).padStart(2, "0")}`;
2078
+ authorization = proofData.authorization;
2079
+ } else {
2080
+ logError("No valid signature found in payment data");
2081
+ return false;
1350
2082
  }
1351
- sql += " ORDER BY created_at DESC";
1352
- if (filters?.limit !== undefined) {
1353
- sql += " LIMIT ?";
1354
- params.push(filters.limit);
2083
+ if (!authorization || typeof authorization !== "object") {
2084
+ logError("No authorization data found in payment data");
2085
+ return false;
1355
2086
  }
1356
- if (filters?.offset !== undefined) {
1357
- sql += " OFFSET ?";
1358
- params.push(filters.offset);
2087
+ if (!authorization.from || !authorization.to || !authorization.value || !authorization.nonce) {
2088
+ logError("Authorization missing required fields");
2089
+ return false;
1359
2090
  }
1360
- const rows = this.db.prepare(sql).all(...params);
1361
- return rows.map((row) => this.rowToRecord(row));
1362
- }
1363
- async getCount(direction, windowMs) {
1364
- let sql = "SELECT COUNT(*) as cnt FROM x402_payments WHERE direction = ? AND status NOT IN ('failed', 'refunded')";
1365
- const params = [direction];
1366
- if (windowMs !== undefined) {
1367
- const cutoff = new Date(Date.now() - windowMs).toISOString();
1368
- sql += " AND created_at >= ?";
1369
- params.push(cutoff);
2091
+ log("Authorization:", {
2092
+ from: `${authorization.from?.substring(0, 10)}...`,
2093
+ to: `${authorization.to?.substring(0, 10)}...`,
2094
+ value: authorization.value
2095
+ });
2096
+ if (!authorization.to) {
2097
+ logError('Authorization missing "to" field');
2098
+ return false;
1370
2099
  }
1371
- const row = this.db.prepare(sql).get(...params);
1372
- return row.cnt;
1373
- }
1374
- async clear() {
1375
- this.db.exec("DELETE FROM x402_payments");
1376
- }
1377
- close() {
1378
- this.db.close();
1379
- }
1380
- rowToRecord(row) {
1381
- let metadata = {};
2100
+ if (authorization.to.toLowerCase() !== expectedRecipient.toLowerCase()) {
2101
+ logError("Recipient mismatch:", authorization.to, "vs", expectedRecipient);
2102
+ return false;
2103
+ }
2104
+ const need = BigInt(expectedAmountAtomic);
2105
+ const authValue = BigInt(authorization.value);
2106
+ if (authValue < need) {
2107
+ logError("Amount too low:", authValue.toString(), "vs", need.toString());
2108
+ return false;
2109
+ }
2110
+ const now = Math.floor(Date.now() / 1000);
2111
+ const validAfter = Number.parseInt(authorization.validAfter || "0", 10);
2112
+ const validBefore = Number.parseInt(authorization.validBefore || String(now + 86400), 10);
2113
+ if (now < validAfter) {
2114
+ logError("Authorization not yet valid:", now, "<", validAfter);
2115
+ return false;
2116
+ }
2117
+ if (now > validBefore) {
2118
+ logError("Authorization expired:", now, ">", validBefore);
2119
+ return false;
2120
+ }
2121
+ log("✓ EIP-712 authorization parameters valid");
2122
+ logSection("Cryptographic Signature Verification");
1382
2123
  try {
1383
- metadata = JSON.parse(row.metadata);
1384
- } catch (_metadataParseError) {}
1385
- return {
1386
- id: row.id,
1387
- direction: row.direction,
1388
- counterparty: row.counterparty,
1389
- amount: BigInt(row.amount),
1390
- network: row.network,
1391
- txHash: row.tx_hash,
1392
- resource: row.resource,
1393
- status: row.status,
1394
- createdAt: row.created_at,
1395
- metadata
1396
- };
2124
+ let verifyingContract;
2125
+ let chainId;
2126
+ let domainName = "USD Coin";
2127
+ let domainVersion = "2";
2128
+ const expectedChainId = getViemChain(network).id;
2129
+ if (proofData.domain && typeof proofData.domain === "object") {
2130
+ const domain2 = proofData.domain;
2131
+ log("Using domain from payment data:", domain2);
2132
+ if (domain2.verifyingContract.toLowerCase() !== expectedVerifyingContract.toLowerCase()) {
2133
+ logError("EIP-712 verifyingContract does not match route token:", domain2.verifyingContract, expectedVerifyingContract);
2134
+ return false;
2135
+ }
2136
+ if (domain2.chainId !== expectedChainId) {
2137
+ logError("EIP-712 chainId mismatch:", domain2.chainId, "expected", expectedChainId);
2138
+ return false;
2139
+ }
2140
+ verifyingContract = domain2.verifyingContract;
2141
+ chainId = domain2.chainId;
2142
+ if (domain2.name)
2143
+ domainName = domain2.name;
2144
+ if (domain2.version)
2145
+ domainVersion = domain2.version;
2146
+ } else {
2147
+ log("No domain in payment data — using expected token + network chain");
2148
+ verifyingContract = expectedVerifyingContract;
2149
+ chainId = expectedChainId;
2150
+ const usdc = getUsdcContractAddress(network);
2151
+ if (expectedVerifyingContract.toLowerCase() === usdc.toLowerCase()) {
2152
+ domainName = "USD Coin";
2153
+ } else {
2154
+ domainName = "Token";
2155
+ }
2156
+ }
2157
+ log("Verifying contract:", verifyingContract, "chainId:", chainId);
2158
+ const domain = {
2159
+ name: domainName,
2160
+ version: domainVersion,
2161
+ chainId,
2162
+ verifyingContract
2163
+ };
2164
+ log("Domain for verification:", domain);
2165
+ const types = {
2166
+ TransferWithAuthorization: TRANSFER_WITH_AUTHORIZATION_TYPES
2167
+ };
2168
+ const message = {
2169
+ from: authorization.from,
2170
+ to: authorization.to,
2171
+ value: BigInt(authorization.value),
2172
+ validAfter: BigInt(authorization.validAfter || 0),
2173
+ validBefore: BigInt(authorization.validBefore || Math.floor(Date.now() / 1000) + 86400),
2174
+ nonce: authorization.nonce
2175
+ };
2176
+ log("Message:", {
2177
+ from: message.from,
2178
+ to: message.to,
2179
+ value: message.value.toString()
2180
+ });
2181
+ try {
2182
+ const recoveredAddress = await recoverTypedDataAddress({
2183
+ domain,
2184
+ types,
2185
+ primaryType: "TransferWithAuthorization",
2186
+ message,
2187
+ signature
2188
+ });
2189
+ log("Recovered signer:", recoveredAddress, "Expected:", authorization.from);
2190
+ const signerMatches = recoveredAddress.toLowerCase() === authorization.from.toLowerCase();
2191
+ if (!signerMatches) {
2192
+ try {
2193
+ const wrongTypeRecovered = await recoverTypedDataAddress({
2194
+ domain,
2195
+ types: {
2196
+ ReceiveWithAuthorization: RECEIVE_WITH_AUTHORIZATION_TYPES
2197
+ },
2198
+ primaryType: "ReceiveWithAuthorization",
2199
+ message,
2200
+ signature
2201
+ });
2202
+ if (wrongTypeRecovered.toLowerCase() === authorization.from.toLowerCase()) {
2203
+ logError("❌ CLIENT ERROR: Wrong EIP-712 type used");
2204
+ return false;
2205
+ }
2206
+ } catch (_e) {
2207
+ log("Could not recover with ReceiveWithAuthorization either");
2208
+ }
2209
+ }
2210
+ log("Signature match:", signerMatches ? "✓ Valid" : "✗ Invalid");
2211
+ if (!signerMatches) {
2212
+ const userAgent = req?.headers?.["user-agent"];
2213
+ const isX402Gateway = typeof userAgent === "string" && userAgent.includes("X402-Gateway");
2214
+ if (isX402Gateway) {
2215
+ log("\uD83D\uDD0D Detected X402 Gateway User-Agent");
2216
+ const trustedSignersSetting = runtime.getSetting("X402_TRUSTED_GATEWAY_SIGNERS");
2217
+ const trustedSigners = typeof trustedSignersSetting === "string" ? trustedSignersSetting : "0x2EB8323f66eE172315503de7325D04c676089267";
2218
+ const signerWhitelist = trustedSigners.split(",").map((addr) => addr.trim().toLowerCase());
2219
+ if (signerWhitelist.includes(recoveredAddress.toLowerCase())) {
2220
+ log("✅ Signature verified: signed by authorized X402 Gateway");
2221
+ return true;
2222
+ } else {
2223
+ logError(`✗ Gateway signer NOT in whitelist: ${recoveredAddress}`);
2224
+ logError(`Add to X402_TRUSTED_GATEWAY_SIGNERS to allow: ${recoveredAddress}`);
2225
+ return false;
2226
+ }
2227
+ } else {
2228
+ logError("✗ Signature verification failed: signer mismatch");
2229
+ logError(`Expected: ${authorization.from}, Actual: ${recoveredAddress}`);
2230
+ return false;
2231
+ }
2232
+ } else {
2233
+ log("✓ Signature cryptographically verified");
2234
+ return true;
2235
+ }
2236
+ } catch (error) {
2237
+ logError("✗ Signature verification failed:", error instanceof Error ? error.message : String(error));
2238
+ return false;
2239
+ }
2240
+ } catch (error) {
2241
+ logError("EIP-712 verification error:", error instanceof Error ? error.message : String(error));
2242
+ return false;
2243
+ }
2244
+ } catch (error) {
2245
+ logError("EIP-712 verification error:", error instanceof Error ? error.message : String(error));
2246
+ return false;
1397
2247
  }
1398
2248
  }
1399
-
1400
- // services/x402-service.ts
1401
- var DEFAULT_FACILITATOR_URL = "https://facilitator.daydreams.systems";
1402
- var DEFAULTS = {
1403
- network: "base",
1404
- maxPaymentUsd: 1,
1405
- maxTotalUsd: 10
1406
- };
1407
-
1408
- class X402Service extends Service {
1409
- static serviceType = "x402_payment";
1410
- capabilityDescription = "x402 HTTP payment protocol - send and receive crypto payments";
1411
- signer = null;
1412
- policyEngine = null;
1413
- circuitBreaker;
1414
- storage;
1415
- serviceConfig;
1416
- fetchWithPayment = null;
1417
- constructor(runtime) {
1418
- super(runtime);
1419
- this.circuitBreaker = new CircuitBreaker;
1420
- this.storage = new MemoryPaymentStorage;
1421
- this.serviceConfig = {
1422
- privateKey: "",
1423
- network: DEFAULTS.network,
1424
- payTo: "",
1425
- facilitatorUrl: DEFAULT_FACILITATOR_URL,
1426
- maxPaymentUsd: DEFAULTS.maxPaymentUsd,
1427
- maxTotalUsd: DEFAULTS.maxTotalUsd,
1428
- enabled: false
1429
- };
1430
- }
1431
- static async start(runtime) {
1432
- const service = new X402Service(runtime);
1433
- await service.initialize(runtime);
1434
- return service;
1435
- }
1436
- async initialize(runtime) {
1437
- this.runtime = runtime;
1438
- const privateKey = String(runtime.getSetting("X402_PRIVATE_KEY") ?? "");
1439
- const network = String(runtime.getSetting("X402_NETWORK") ?? DEFAULTS.network);
1440
- const payTo = String(runtime.getSetting("X402_PAY_TO") ?? "");
1441
- const facilitatorUrl = String(runtime.getSetting("X402_FACILITATOR_URL") ?? DEFAULT_FACILITATOR_URL);
1442
- const maxPaymentUsdRaw = runtime.getSetting("X402_MAX_PAYMENT_USD");
1443
- const maxPaymentUsd = maxPaymentUsdRaw !== null ? parseFloat(String(maxPaymentUsdRaw)) : DEFAULTS.maxPaymentUsd;
1444
- const maxTotalUsdRaw = runtime.getSetting("X402_MAX_TOTAL_USD");
1445
- const maxTotalUsd = maxTotalUsdRaw !== null ? parseFloat(String(maxTotalUsdRaw)) : DEFAULTS.maxTotalUsd;
1446
- const enabledSetting = runtime.getSetting("X402_ENABLED");
1447
- const enabled = String(enabledSetting) !== "false" && privateKey.length > 0;
1448
- this.serviceConfig = {
1449
- privateKey,
1450
- network,
1451
- payTo,
1452
- facilitatorUrl,
1453
- maxPaymentUsd: isNaN(maxPaymentUsd) ? DEFAULTS.maxPaymentUsd : maxPaymentUsd,
1454
- maxTotalUsd: isNaN(maxTotalUsd) ? DEFAULTS.maxTotalUsd : maxTotalUsd,
1455
- enabled
1456
- };
1457
- if (!enabled) {
1458
- logger5.info("[x402] Service inactive — no private key configured or explicitly disabled");
2249
+ function createPaymentAwareHandler(route) {
2250
+ const originalHandler = route.handler;
2251
+ return async (req, res, runtime) => {
2252
+ const typedReq = req;
2253
+ const typedRes = res;
2254
+ const typedRuntime = runtime;
2255
+ if (route.x402 == null) {
2256
+ if (originalHandler) {
2257
+ return originalHandler(req, res, runtime);
2258
+ }
1459
2259
  return;
1460
2260
  }
1461
- try {
1462
- resolveNetwork(network);
1463
- } catch (err) {
1464
- const message = err instanceof Error ? err.message : String(err);
1465
- logger5.error(`[x402] Invalid network configuration: ${message}`);
1466
- this.serviceConfig.enabled = false;
2261
+ const testMode = process.env.X402_TEST_MODE === "true" || process.env.X402_TEST_MODE === "1";
2262
+ if (testMode) {
2263
+ logger5.warn("[@elizaos/agent x402] X402_TEST_MODE is set — skipping payment verification (development only)");
2264
+ if (originalHandler) {
2265
+ return originalHandler(req, res, runtime);
2266
+ }
1467
2267
  return;
1468
2268
  }
1469
- try {
1470
- this.signer = new EvmPaymentSigner(privateKey, network);
1471
- logger5.info(`[x402] Wallet initialized: ${this.signer.address} on ${network}`);
1472
- } catch (err) {
1473
- const message = err instanceof Error ? err.message : String(err);
1474
- logger5.error(`[x402] Failed to initialize signer: ${message}`);
1475
- this.serviceConfig.enabled = false;
2269
+ const x402Cfg = resolveEffectiveX402(route, typedRuntime);
2270
+ if (!x402Cfg) {
2271
+ if (!typedRes.headersSent) {
2272
+ typedRes.status(500).json({
2273
+ error: "x402 misconfiguration",
2274
+ message: "Could not resolve x402 price/paymentConfigs. For `x402: true`, set character.settings.x402.defaultPriceInCents and defaultPaymentConfigs. For partial x402 on the route, supply priceInCents and paymentConfigs or the matching character defaults.",
2275
+ path: route.path
2276
+ });
2277
+ }
1476
2278
  return;
1477
2279
  }
1478
- this.serviceConfig.privateKey = "";
1479
- const dbPath = String(runtime.getSetting("X402_DB_PATH") ?? "");
1480
- if (dbPath) {
2280
+ const payRoute = { ...route, x402: x402Cfg };
2281
+ logSection(`X402 Payment Check - ${route.path}`);
2282
+ log("Method:", typedReq.method);
2283
+ if (route.validator) {
1481
2284
  try {
1482
- this.storage = new SqlitePaymentStorage(dbPath);
1483
- logger5.info(`[x402] Using SQLite storage at ${dbPath}`);
1484
- } catch (err) {
1485
- const message = err instanceof Error ? err.message : String(err);
1486
- logger5.warn(`[x402] Failed to initialize SQLite storage: ${message}. Falling back to memory storage.`);
1487
- this.storage = new MemoryPaymentStorage;
1488
- }
1489
- } else {
1490
- logger5.info("[x402] Using in-memory storage (set X402_DB_PATH for persistence)");
1491
- }
1492
- const policy = {
1493
- outgoing: {
1494
- maxPerTransaction: usdToBaseUnits(this.serviceConfig.maxPaymentUsd),
1495
- maxTotal: usdToBaseUnits(this.serviceConfig.maxTotalUsd),
1496
- windowMs: ONE_DAY_MS,
1497
- maxTransactions: 1000,
1498
- allowedRecipients: [],
1499
- blockedRecipients: []
1500
- },
1501
- incoming: {
1502
- minPerTransaction: 0n,
1503
- allowedSenders: [],
1504
- blockedSenders: []
2285
+ const validationResult = await route.validator(typedReq);
2286
+ if (!validationResult.valid) {
2287
+ logError("✗ Validation failed:", validationResult.error?.message);
2288
+ const x402Response = buildX402Response(payRoute, typedRuntime);
2289
+ typedRuntime.emitEvent(X402_EVENT_PAYMENT_REQUIRED, {
2290
+ path: route.path,
2291
+ configNames: payRoute.x402.paymentConfigs ?? ["base_usdc"],
2292
+ reason: "validator_failed"
2293
+ });
2294
+ const errorMessage = validationResult.error?.details ? `${validationResult.error.message}: ${JSON.stringify(validationResult.error.details)}` : validationResult.error?.message || "Invalid request parameters";
2295
+ setStandardPaymentRequiredHeaders(typedRes, payRoute, typedRuntime, errorMessage);
2296
+ return typedRes.status(402).json({
2297
+ ...x402Response,
2298
+ error: errorMessage
2299
+ });
2300
+ }
2301
+ log("✓ Validation passed");
2302
+ } catch (error) {
2303
+ logError("✗ Validation error:", error instanceof Error ? error.message : String(error));
2304
+ const x402Response = buildX402Response(payRoute, typedRuntime);
2305
+ typedRuntime.emitEvent(X402_EVENT_PAYMENT_REQUIRED, {
2306
+ path: route.path,
2307
+ configNames: payRoute.x402.paymentConfigs ?? ["base_usdc"],
2308
+ reason: "validator_error"
2309
+ });
2310
+ setStandardPaymentRequiredHeaders(typedRes, payRoute, typedRuntime, "Validation error");
2311
+ return typedRes.status(402).json({
2312
+ ...x402Response,
2313
+ error: `Validation error: ${error instanceof Error ? error.message : "Unknown error"}`
2314
+ });
1505
2315
  }
1506
- };
1507
- this.policyEngine = new PolicyEngine(policy, this.storage);
1508
- this.circuitBreaker = new CircuitBreaker;
1509
- this.fetchWithPayment = createFetchWithPayment({
1510
- signer: this.signer,
1511
- policyEngine: this.policyEngine,
1512
- circuitBreaker: this.circuitBreaker,
1513
- storage: this.storage,
1514
- logger: logger5
2316
+ }
2317
+ const requestHeaders = typedReq.headers ?? {};
2318
+ const requestQuery = typedReq.query ?? {};
2319
+ log("Headers:", JSON.stringify(requestHeaders, null, 2));
2320
+ log("Query:", JSON.stringify(requestQuery, null, 2));
2321
+ if (typedReq.method === "POST" && typedReq.body) {
2322
+ log("Body:", JSON.stringify(typedReq.body, null, 2));
2323
+ }
2324
+ const paymentProof = requestHeaders["x-payment-proof"] || requestHeaders["x-payment"] || requestHeaders["payment-signature"] || requestQuery.paymentProof;
2325
+ const paymentId = requestHeaders["x-payment-id"] || requestQuery.paymentId;
2326
+ log("Payment credentials:", {
2327
+ "x-payment-proof": !!requestHeaders["x-payment-proof"],
2328
+ "x-payment": !!requestHeaders["x-payment"],
2329
+ "payment-signature": !!requestHeaders["payment-signature"],
2330
+ "x-payment-id": !!paymentId,
2331
+ found: !!(paymentProof || paymentId)
1515
2332
  });
1516
- logger5.info(`[x402] Service active — max per-txn: $${this.serviceConfig.maxPaymentUsd}, max daily: $${this.serviceConfig.maxTotalUsd}`);
1517
- }
1518
- async stop() {
1519
- logger5.info("[x402] Service stopping");
1520
- this.signer = null;
1521
- this.fetchWithPayment = null;
1522
- }
1523
- getFetchWithPayment() {
1524
- if (!this.fetchWithPayment) {
1525
- return (input, init) => fetch(input, init);
1526
- }
1527
- return this.fetchWithPayment;
1528
- }
1529
- async getSummary(windowMs = ONE_DAY_MS) {
1530
- const [totalSpent, totalEarned, outgoingCount, incomingCount] = await Promise.all([
1531
- this.storage.getTotal("outgoing", windowMs),
1532
- this.storage.getTotal("incoming", windowMs),
1533
- this.storage.getCount("outgoing", windowMs),
1534
- this.storage.getCount("incoming", windowMs)
1535
- ]);
1536
- return {
1537
- totalSpent,
1538
- totalEarned,
1539
- outgoingCount,
1540
- incomingCount,
1541
- windowMs
1542
- };
1543
- }
1544
- async getRecentTransactions(limit = 20) {
1545
- return this.storage.getRecords({ limit });
1546
- }
1547
- isActive() {
1548
- return this.serviceConfig.enabled && this.signer !== null;
1549
- }
1550
- canMakePayments() {
1551
- return this.isActive() && this.fetchWithPayment !== null;
1552
- }
1553
- updatePolicy(policy) {
1554
- if (this.policyEngine) {
1555
- this.policyEngine.updatePolicy(policy);
1556
- logger5.info("[x402] Payment policy updated");
2333
+ if (paymentProof || paymentId) {
2334
+ log("Payment credentials received:", {
2335
+ proofLength: paymentProof ? String(paymentProof).length : 0,
2336
+ paymentId
2337
+ });
2338
+ try {
2339
+ const cfgNames = payRoute.x402.paymentConfigs ?? ["base_usdc"];
2340
+ const outcome = await verifyPayment({
2341
+ paymentProof: typeof paymentProof === "string" ? paymentProof : undefined,
2342
+ paymentId: typeof paymentId === "string" ? paymentId : undefined,
2343
+ route: route.path,
2344
+ priceInCents: payRoute.x402.priceInCents,
2345
+ paymentConfigNames: cfgNames,
2346
+ agentId: typedRuntime.agentId ? String(typedRuntime.agentId) : undefined,
2347
+ runtime: typedRuntime,
2348
+ req: typedReq
2349
+ });
2350
+ if (outcome.ok) {
2351
+ log("✓ PAYMENT VERIFIED - executing handler");
2352
+ typedRuntime.emitEvent(X402_EVENT_PAYMENT_VERIFIED, {
2353
+ path: route.path,
2354
+ priceInCents: payRoute.x402.priceInCents,
2355
+ paymentConfigs: payRoute.x402.paymentConfigs,
2356
+ payer: outcome.details.payer,
2357
+ amountAtomic: outcome.details.amountAtomic,
2358
+ network: outcome.details.network,
2359
+ proofId: outcome.details.proofId,
2360
+ paymentConfig: outcome.details.paymentConfig,
2361
+ symbol: outcome.details.symbol
2362
+ });
2363
+ if (outcome.details.paymentResponse && typedRes.setHeader) {
2364
+ typedRes.setHeader("PAYMENT-RESPONSE", outcome.details.paymentResponse);
2365
+ typedRes.setHeader("Access-Control-Expose-Headers", "PAYMENT-REQUIRED, PAYMENT-RESPONSE, Payment-Required, Payment-Response");
2366
+ }
2367
+ if (originalHandler) {
2368
+ return originalHandler(req, res, runtime);
2369
+ }
2370
+ return;
2371
+ }
2372
+ logError("✗ PAYMENT VERIFICATION FAILED");
2373
+ const x402Base = buildX402Response(payRoute, typedRuntime);
2374
+ typedRuntime.emitEvent(X402_EVENT_PAYMENT_REQUIRED, {
2375
+ path: route.path,
2376
+ configNames: cfgNames,
2377
+ reason: "verification_failed"
2378
+ });
2379
+ setStandardPaymentRequiredHeaders(typedRes, payRoute, typedRuntime, "Payment verification failed");
2380
+ typedRes.status(402).json({
2381
+ ...x402Base,
2382
+ error: "Payment verification failed",
2383
+ message: "The provided payment proof is invalid or has expired, or the amount or token does not match this route."
2384
+ });
2385
+ return;
2386
+ } catch (error) {
2387
+ logError("✗ PAYMENT VERIFICATION ERROR:", error instanceof Error ? error.message : String(error));
2388
+ let x402Base;
2389
+ try {
2390
+ x402Base = buildX402Response(payRoute, typedRuntime);
2391
+ } catch {
2392
+ x402Base = createX402Response({
2393
+ error: "Payment verification error"
2394
+ });
2395
+ }
2396
+ typedRuntime.emitEvent(X402_EVENT_PAYMENT_REQUIRED, {
2397
+ path: route.path,
2398
+ configNames: payRoute.x402.paymentConfigs ?? ["base_usdc"],
2399
+ reason: "verification_error"
2400
+ });
2401
+ setStandardPaymentRequiredHeaders(typedRes, payRoute, typedRuntime, "Payment verification error");
2402
+ typedRes.status(402).json({
2403
+ ...x402Base,
2404
+ error: "Payment verification error",
2405
+ message: error instanceof Error ? error.message : String(error)
2406
+ });
2407
+ return;
2408
+ }
1557
2409
  }
1558
- }
1559
- getWalletAddress() {
1560
- return this.signer?.address ?? null;
1561
- }
1562
- getNetwork() {
1563
- return this.serviceConfig.network;
1564
- }
1565
- getFacilitatorUrl() {
1566
- return this.serviceConfig.facilitatorUrl;
1567
- }
1568
- getPayToAddress() {
1569
- return this.serviceConfig.payTo;
1570
- }
1571
- getStorage() {
1572
- return this.storage;
1573
- }
1574
- getCircuitBreakerState() {
1575
- return this.circuitBreaker.getState();
1576
- }
1577
- resetCircuitBreaker() {
1578
- this.circuitBreaker.reset();
1579
- logger5.info("[x402] Circuit breaker reset");
1580
- }
1581
- }
1582
- // middleware/facilitator-client.ts
1583
- function buildFacilitatorRequestBody(paymentProof, requirement) {
1584
- let paymentPayload;
1585
- try {
1586
- const decoded = Buffer.from(paymentProof, "base64").toString("utf-8");
1587
- paymentPayload = JSON.parse(decoded);
1588
- } catch {
2410
+ log("No payment credentials - returning 402");
1589
2411
  try {
1590
- paymentPayload = JSON.parse(paymentProof);
1591
- } catch {
1592
- throw new Error("Payment proof is neither valid base64-encoded JSON nor direct JSON");
2412
+ const x402Response = buildX402Response(payRoute, typedRuntime);
2413
+ typedRuntime.emitEvent(X402_EVENT_PAYMENT_REQUIRED, {
2414
+ path: route.path,
2415
+ configNames: payRoute.x402.paymentConfigs ?? ["base_usdc"],
2416
+ reason: "payment_required"
2417
+ });
2418
+ log("Payment options:", {
2419
+ paymentConfigs: payRoute.x402.paymentConfigs || ["base_usdc"],
2420
+ priceInCents: payRoute.x402.priceInCents,
2421
+ count: x402Response.accepts?.length || 0
2422
+ });
2423
+ log("402 Response:", JSON.stringify(x402Response, null, 2));
2424
+ setStandardPaymentRequiredHeaders(typedRes, payRoute, typedRuntime, "Payment Required");
2425
+ typedRes.status(402).json(x402Response);
2426
+ } catch (error) {
2427
+ logError("✗ Failed to build x402 response:", error instanceof Error ? error.message : String(error));
2428
+ typedRes.status(402).json(createX402Response({
2429
+ error: `Payment Required: ${error instanceof Error ? error.message : "Unknown error"}`
2430
+ }));
1593
2431
  }
1594
- }
1595
- const paymentRequirements = {
1596
- scheme: requirement.scheme,
1597
- network: requirement.network,
1598
- asset: requirement.asset,
1599
- amount: requirement.maxAmountRequired,
1600
- payTo: requirement.payTo,
1601
- maxTimeoutSeconds: requirement.maxTimeoutSeconds,
1602
- extra: requirement.extra
1603
2432
  };
1604
- return JSON.stringify({ paymentPayload, paymentRequirements });
1605
2433
  }
1606
- async function verifyPaymentWithFacilitator(paymentProof, facilitatorUrl, requirement) {
1607
- const url = new URL("/verify", facilitatorUrl);
1608
- try {
1609
- const body = buildFacilitatorRequestBody(paymentProof, requirement);
1610
- const response = await fetch(url.toString(), {
1611
- method: "POST",
1612
- headers: {
1613
- "Content-Type": "application/json"
2434
+ function setStandardPaymentRequiredHeaders(res, route, runtime, error = "Payment Required") {
2435
+ if (!res.setHeader || res.headersSent)
2436
+ return;
2437
+ const paymentConfigNames = route.x402.paymentConfigs || ["base_usdc"];
2438
+ const agentId = runtime?.agentId ? String(runtime.agentId) : undefined;
2439
+ const paymentRequired = buildStandardPaymentRequired({
2440
+ routePath: route.path,
2441
+ description: generateDescription(route),
2442
+ priceInCents: route.x402.priceInCents,
2443
+ paymentConfigNames,
2444
+ agentId,
2445
+ error
2446
+ });
2447
+ const encoded = Buffer.from(JSON.stringify(paymentRequired), "utf8").toString("base64");
2448
+ res.setHeader("PAYMENT-REQUIRED", encoded);
2449
+ res.setHeader("Access-Control-Expose-Headers", "PAYMENT-REQUIRED, PAYMENT-RESPONSE, Payment-Required, Payment-Response");
2450
+ }
2451
+ function buildX402Response(route, runtime) {
2452
+ if (!route.x402.priceInCents) {
2453
+ throw new Error("Route x402.priceInCents is required for x402 response");
2454
+ }
2455
+ const paymentConfigs = route.x402.paymentConfigs || ["base_usdc"];
2456
+ const agentId = runtime?.agentId ? String(runtime.agentId) : undefined;
2457
+ const accepts = paymentConfigs.flatMap((configName) => {
2458
+ const config = getPaymentConfig(configName, agentId);
2459
+ const caip19 = getCAIP19FromConfig(config);
2460
+ const maxAmountRequired = atomicAmountForPriceInCents(route.x402.priceInCents, config);
2461
+ const inputSchema = buildInputSchemaFromRoute(route);
2462
+ const method = route.type === "POST" ? "POST" : "GET";
2463
+ const outputSchema = {
2464
+ input: {
2465
+ type: "http",
2466
+ method,
2467
+ bodyType: method === "POST" ? "json" : undefined,
2468
+ pathParams: inputSchema.pathParams,
2469
+ queryParams: inputSchema.queryParams,
2470
+ bodyFields: inputSchema.bodyFields,
2471
+ headerFields: {
2472
+ "X-Payment": {
2473
+ type: "string",
2474
+ required: false,
2475
+ description: "Standard x402 payment header (base64-encoded JSON or raw JSON with x402Version, accepted, payload) — verified via facilitator POST when configured"
2476
+ },
2477
+ "X-Payment-Proof": {
2478
+ type: "string",
2479
+ required: false,
2480
+ description: "Legacy payment proof (tx hash, colon-delimited, or JSON)"
2481
+ },
2482
+ "X-Payment-Id": {
2483
+ type: "string",
2484
+ required: false,
2485
+ description: "Optional payment ID for tracking"
2486
+ }
2487
+ }
1614
2488
  },
1615
- body
1616
- });
1617
- if (!response.ok) {
1618
- const errorText = await response.text().catch(() => "unknown error");
1619
- return {
1620
- valid: false,
1621
- reason: `Facilitator returned ${response.status}: ${errorText}`
1622
- };
1623
- }
1624
- const result = await response.json();
1625
- return {
1626
- valid: result.isValid === true,
1627
- payer: result.payer,
1628
- reason: result.invalidReason
2489
+ output: {
2490
+ type: "object",
2491
+ description: "API response data (varies by endpoint)"
2492
+ }
1629
2493
  };
1630
- } catch (err) {
1631
- const message = err instanceof Error ? err.message : String(err);
1632
- return {
1633
- valid: false,
1634
- reason: `Facilitator request failed: ${message}`
2494
+ const extra = {
2495
+ priceInCents: route.x402.priceInCents || 0,
2496
+ priceUSD: `$${((route.x402.priceInCents || 0) / 100).toFixed(2)}`,
2497
+ symbol: config.symbol,
2498
+ paymentConfig: configName,
2499
+ expiresIn: 300
1635
2500
  };
2501
+ if (config.network === "BASE" || config.network === "POLYGON" || config.network === "BSC") {
2502
+ const isUsdc = config.symbol?.toUpperCase() === "USDC";
2503
+ const tokenName = isUsdc ? "USD Coin" : config.symbol || "Token";
2504
+ extra.name = tokenName;
2505
+ extra.version = "2";
2506
+ extra.eip712Domain = {
2507
+ name: tokenName,
2508
+ version: "2",
2509
+ chainId: Number.parseInt(config.chainId || "1", 10),
2510
+ verifyingContract: config.assetReference
2511
+ };
2512
+ }
2513
+ return createAccepts({
2514
+ network: toX402Network(config.network),
2515
+ maxAmountRequired,
2516
+ resource: toResourceUrl(route.path),
2517
+ description: generateDescription(route),
2518
+ payTo: config.paymentAddress,
2519
+ asset: caip19,
2520
+ mimeType: "application/json",
2521
+ maxTimeoutSeconds: 300,
2522
+ outputSchema,
2523
+ extra
2524
+ });
2525
+ });
2526
+ return createX402Response({
2527
+ accepts,
2528
+ error: "Payment Required"
2529
+ });
2530
+ }
2531
+ function extractPathParams(path) {
2532
+ const matches = path.matchAll(/:([^/]+)/g);
2533
+ return Array.from(matches, (m) => m[1]);
2534
+ }
2535
+ function isRecord(value) {
2536
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2537
+ }
2538
+ function isOpenAPIObjectSchema(schema) {
2539
+ if (!isRecord(schema) || schema.type !== "object") {
2540
+ return false;
1636
2541
  }
2542
+ const properties = schema.properties;
2543
+ return properties === undefined || isRecord(properties) && Object.values(properties).every((property) => property === undefined || isRecord(property));
1637
2544
  }
1638
- async function settlePaymentWithFacilitator(paymentProof, facilitatorUrl, requirement) {
1639
- const url = new URL("/settle", facilitatorUrl);
1640
- try {
1641
- const body = buildFacilitatorRequestBody(paymentProof, requirement);
1642
- const response = await fetch(url.toString(), {
1643
- method: "POST",
1644
- headers: {
1645
- "Content-Type": "application/json"
1646
- },
1647
- body
1648
- });
1649
- if (!response.ok) {
1650
- const errorText = await response.text().catch(() => "unknown error");
1651
- return {
1652
- success: false,
1653
- reason: `Facilitator returned ${response.status}: ${errorText}`
2545
+ function convertOpenAPISchemaToFieldDef(schema) {
2546
+ if ("properties" in schema && schema.properties) {
2547
+ const fields = {};
2548
+ for (const [key, value] of Object.entries(schema.properties)) {
2549
+ fields[key] = {
2550
+ type: value.type,
2551
+ required: "required" in schema && schema.required ? schema.required.includes(key) : false,
2552
+ description: value.description,
2553
+ enum: value.enum,
2554
+ pattern: value.pattern,
2555
+ properties: value.properties ? convertOpenAPISchemaToFieldDef(value) : undefined
1654
2556
  };
1655
2557
  }
1656
- const result = await response.json();
1657
- return {
1658
- success: result.success === true,
1659
- txHash: result.transaction,
1660
- reason: result.errorReason
1661
- };
1662
- } catch (err) {
1663
- const message = err instanceof Error ? err.message : String(err);
1664
- return {
1665
- success: false,
1666
- reason: `Facilitator request failed: ${message}`
1667
- };
2558
+ return fields;
1668
2559
  }
2560
+ return {};
1669
2561
  }
1670
-
1671
- // middleware/paywall.ts
1672
- function createPaywallMiddleware(config, storage, onStorageError) {
1673
- const networkInfo = resolveNetwork(config.network);
1674
- const requirementTemplate = {
1675
- scheme: "upto",
1676
- network: networkInfo.caip2,
1677
- maxAmountRequired: config.amount.toString(),
1678
- resource: "",
1679
- description: config.description,
1680
- mimeType: config.mimeType,
1681
- payTo: config.payTo,
1682
- maxTimeoutSeconds: config.maxTimeoutSeconds,
1683
- asset: networkInfo.usdcAddress,
1684
- extra: {
1685
- name: networkInfo.usdcDomainName,
1686
- version: networkInfo.usdcPermitVersion
1687
- }
1688
- };
1689
- return async (req, res, next) => {
1690
- const paymentHeader = getHeader(req, "x-payment");
1691
- if (!paymentHeader) {
1692
- const resource = req.url ?? "/";
1693
- const requirement2 = { ...requirementTemplate, resource };
1694
- const paymentRequired = {
1695
- x402Version: 2,
1696
- accepts: [requirement2]
2562
+ function buildInputSchemaFromRoute(route) {
2563
+ const schema = {};
2564
+ if (route.openapi?.parameters) {
2565
+ const pathParams = {};
2566
+ for (const p of route.openapi.parameters.filter((x) => x.in === "path")) {
2567
+ pathParams[p.name] = {
2568
+ type: p.schema.type,
2569
+ required: p.required ?? true,
2570
+ description: p.description,
2571
+ enum: p.schema.enum,
2572
+ pattern: p.schema.pattern
1697
2573
  };
1698
- const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
1699
- res.setHeader("Payment-Required", encoded).setHeader("x-402", encoded).status(402).json({
1700
- error: "Payment Required",
1701
- message: config.description,
1702
- x402Version: 2
1703
- });
1704
- return;
1705
- }
1706
- const requirement = {
1707
- ...requirementTemplate,
1708
- resource: req.url ?? "/"
1709
- };
1710
- const verifyResult = await verifyPaymentWithFacilitator(paymentHeader, config.facilitatorUrl, requirement);
1711
- if (!verifyResult.valid) {
1712
- res.status(402).json({
1713
- error: "Payment Invalid",
1714
- reason: verifyResult.reason ?? "Payment verification failed"
1715
- });
1716
- return;
1717
- }
1718
- const settleResult = await settlePaymentWithFacilitator(paymentHeader, config.facilitatorUrl, requirement);
1719
- if (storage && verifyResult.payer) {
1720
- const id = `x402_in_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
1721
- await storage.recordPayment({
1722
- id,
1723
- direction: "incoming",
1724
- counterparty: verifyResult.payer,
1725
- amount: config.amount,
1726
- network: networkInfo.caip2,
1727
- txHash: settleResult.txHash ?? "",
1728
- resource: req.url ?? "/",
1729
- status: settleResult.success ? "confirmed" : "pending",
1730
- createdAt: new Date().toISOString(),
1731
- metadata: {}
1732
- }).catch((err) => {
1733
- if (onStorageError) {
1734
- onStorageError(err);
1735
- }
1736
- });
1737
- }
1738
- if (settleResult.txHash) {
1739
- res.setHeader("x-upto-session-id", settleResult.txHash);
1740
- res.setHeader("X-PAYMENT-RESPONSE", settleResult.txHash);
1741
2574
  }
1742
- next();
1743
- };
1744
- }
1745
- function getHeader(req, name) {
1746
- const headers = req.headers;
1747
- const lowerName = name.toLowerCase();
1748
- for (const [key, value] of Object.entries(headers)) {
1749
- if (key.toLowerCase() === lowerName) {
1750
- if (Array.isArray(value)) {
1751
- return value[0];
2575
+ if (Object.keys(pathParams).length > 0)
2576
+ schema.pathParams = pathParams;
2577
+ } else {
2578
+ const paramNames = extractPathParams(route.path);
2579
+ if (paramNames.length > 0) {
2580
+ const pathParams = {};
2581
+ for (const name of paramNames) {
2582
+ pathParams[name] = {
2583
+ type: "string",
2584
+ required: true,
2585
+ description: `Path parameter: ${name}`
2586
+ };
1752
2587
  }
1753
- return value;
2588
+ schema.pathParams = pathParams;
2589
+ }
2590
+ }
2591
+ if (route.openapi?.parameters) {
2592
+ const queryParams = {};
2593
+ for (const p of route.openapi.parameters.filter((x) => x.in === "query")) {
2594
+ queryParams[p.name] = {
2595
+ type: p.schema.type,
2596
+ required: p.required ?? false,
2597
+ description: p.description,
2598
+ enum: p.schema.enum,
2599
+ pattern: p.schema.pattern
2600
+ };
1754
2601
  }
2602
+ if (Object.keys(queryParams).length > 0)
2603
+ schema.queryParams = queryParams;
1755
2604
  }
1756
- return;
1757
- }
1758
- // storage/postgres.ts
1759
- import pg from "pg";
1760
- var { Pool } = pg;
1761
-
1762
- class PostgresPaymentStorage {
1763
- pool;
1764
- agentId;
1765
- initialized;
1766
- constructor(connectionString, agentId) {
1767
- this.pool = new Pool({ connectionString });
1768
- this.agentId = agentId;
1769
- this.initialized = this.initialize();
1770
- }
1771
- async initialize() {
1772
- const client = await this.pool.connect();
1773
- try {
1774
- await client.query(`
1775
- CREATE TABLE IF NOT EXISTS x402_payments (
1776
- id TEXT PRIMARY KEY,
1777
- agent_id TEXT NOT NULL,
1778
- direction VARCHAR NOT NULL CHECK(direction IN ('outgoing', 'incoming')),
1779
- counterparty TEXT NOT NULL,
1780
- amount TEXT NOT NULL,
1781
- network TEXT NOT NULL,
1782
- tx_hash TEXT NOT NULL DEFAULT '',
1783
- resource TEXT NOT NULL DEFAULT '',
1784
- status TEXT NOT NULL DEFAULT 'pending',
1785
- created_at TEXT NOT NULL,
1786
- metadata JSONB NOT NULL DEFAULT '{}'
1787
- );
1788
- CREATE INDEX IF NOT EXISTS idx_x402_agent ON x402_payments(agent_id);
1789
- CREATE INDEX IF NOT EXISTS idx_x402_direction ON x402_payments(agent_id, direction, created_at);
1790
- `);
1791
- } finally {
1792
- client.release();
1793
- }
1794
- }
1795
- async ready() {
1796
- await this.initialized;
1797
- }
1798
- async recordPayment(record) {
1799
- await this.ready();
1800
- await this.pool.query(`INSERT INTO x402_payments (id, agent_id, direction, counterparty, amount, network, tx_hash, resource, status, created_at, metadata)
1801
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [
1802
- record.id,
1803
- this.agentId,
1804
- record.direction,
1805
- record.counterparty,
1806
- record.amount.toString(),
1807
- record.network,
1808
- record.txHash,
1809
- record.resource,
1810
- record.status,
1811
- record.createdAt,
1812
- JSON.stringify(record.metadata)
1813
- ]);
1814
- }
1815
- async getTotal(direction, windowMs, scope) {
1816
- await this.ready();
1817
- let sql = "SELECT amount FROM x402_payments WHERE agent_id = $1 AND direction = $2 AND status NOT IN ('failed', 'refunded')";
1818
- const params = [this.agentId, direction];
1819
- let paramIdx = 3;
1820
- if (windowMs !== undefined) {
1821
- const cutoff = new Date(Date.now() - windowMs).toISOString();
1822
- sql += ` AND created_at >= $${paramIdx}`;
1823
- params.push(cutoff);
1824
- paramIdx++;
1825
- }
1826
- if (scope) {
1827
- sql += ` AND counterparty = $${paramIdx}`;
1828
- params.push(scope);
1829
- paramIdx++;
1830
- }
1831
- const result = await this.pool.query(sql, params);
1832
- let total = 0n;
1833
- for (const row of result.rows) {
1834
- total += BigInt(row.amount);
1835
- }
1836
- return total;
1837
- }
1838
- async getRecords(filters) {
1839
- await this.ready();
1840
- let sql = "SELECT * FROM x402_payments WHERE agent_id = $1";
1841
- const params = [this.agentId];
1842
- let paramIdx = 2;
1843
- if (filters) {
1844
- if (filters.direction) {
1845
- sql += ` AND direction = $${paramIdx}`;
1846
- params.push(filters.direction);
1847
- paramIdx++;
1848
- }
1849
- if (filters.counterparty) {
1850
- sql += ` AND LOWER(counterparty) = LOWER($${paramIdx})`;
1851
- params.push(filters.counterparty);
1852
- paramIdx++;
1853
- }
1854
- if (filters.status) {
1855
- sql += ` AND status = $${paramIdx}`;
1856
- params.push(filters.status);
1857
- paramIdx++;
1858
- }
1859
- if (filters.network) {
1860
- sql += ` AND network = $${paramIdx}`;
1861
- params.push(filters.network);
1862
- paramIdx++;
1863
- }
1864
- if (filters.since) {
1865
- sql += ` AND created_at >= $${paramIdx}`;
1866
- params.push(filters.since);
1867
- paramIdx++;
1868
- }
1869
- if (filters.until) {
1870
- sql += ` AND created_at <= $${paramIdx}`;
1871
- params.push(filters.until);
1872
- paramIdx++;
1873
- }
2605
+ if (route.openapi?.requestBody?.content?.["application/json"]?.schema) {
2606
+ const requestBodySchema = route.openapi.requestBody.content["application/json"].schema;
2607
+ if (isOpenAPIObjectSchema(requestBodySchema)) {
2608
+ schema.bodyFields = convertOpenAPISchemaToFieldDef(requestBodySchema);
1874
2609
  }
1875
- sql += " ORDER BY created_at DESC";
1876
- if (filters?.limit !== undefined) {
1877
- sql += ` LIMIT $${paramIdx}`;
1878
- params.push(filters.limit);
1879
- paramIdx++;
1880
- }
1881
- if (filters?.offset !== undefined) {
1882
- sql += ` OFFSET $${paramIdx}`;
1883
- params.push(filters.offset);
1884
- paramIdx++;
1885
- }
1886
- const result = await this.pool.query(sql, params);
1887
- return result.rows.map((row) => this.rowToRecord(row));
1888
- }
1889
- async getCount(direction, windowMs) {
1890
- await this.ready();
1891
- let sql = "SELECT COUNT(*) as cnt FROM x402_payments WHERE agent_id = $1 AND direction = $2 AND status NOT IN ('failed', 'refunded')";
1892
- const params = [this.agentId, direction];
1893
- let paramIdx = 3;
1894
- if (windowMs !== undefined) {
1895
- const cutoff = new Date(Date.now() - windowMs).toISOString();
1896
- sql += ` AND created_at >= $${paramIdx}`;
1897
- params.push(cutoff);
1898
- paramIdx++;
1899
- }
1900
- const result = await this.pool.query(sql, params);
1901
- return parseInt(result.rows[0].cnt, 10);
1902
- }
1903
- async clear() {
1904
- await this.ready();
1905
- await this.pool.query("DELETE FROM x402_payments WHERE agent_id = $1", [
1906
- this.agentId
1907
- ]);
1908
- }
1909
- async close() {
1910
- await this.pool.end();
1911
- }
1912
- rowToRecord(row) {
1913
- let metadata = {};
1914
- try {
1915
- metadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
1916
- } catch (_metadataParseError) {}
1917
- return {
1918
- id: row.id,
1919
- direction: row.direction,
1920
- counterparty: row.counterparty,
1921
- amount: BigInt(row.amount),
1922
- network: row.network,
1923
- txHash: row.tx_hash,
1924
- resource: row.resource,
1925
- status: row.status,
1926
- createdAt: row.created_at,
1927
- metadata
1928
- };
1929
2610
  }
2611
+ return schema;
1930
2612
  }
1931
-
1932
- // index.ts
1933
- async function handleSummary(_req, res, runtime) {
1934
- const service = runtime.getService("x402_payment");
1935
- if (!service || !service.isActive()) {
1936
- res.status(503).json({ error: "x402 service not active" });
1937
- return;
1938
- }
1939
- const summary = await service.getSummary();
1940
- res.status(200).json({
1941
- wallet: service.getWalletAddress() ?? "",
1942
- network: service.getNetwork(),
1943
- totalSpent: formatUsd(summary.totalSpent),
1944
- totalSpentRaw: summary.totalSpent.toString(),
1945
- totalEarned: formatUsd(summary.totalEarned),
1946
- totalEarnedRaw: summary.totalEarned.toString(),
1947
- outgoingCount: summary.outgoingCount,
1948
- incomingCount: summary.incomingCount,
1949
- windowMs: summary.windowMs,
1950
- circuitBreaker: service.getCircuitBreakerState()
1951
- });
2613
+ function generateDescription(route) {
2614
+ if (route.description)
2615
+ return route.description;
2616
+ const pathParts = route.path.split("/").filter(Boolean);
2617
+ const action = route.type.toLowerCase() === "get" ? "Get" : "Execute";
2618
+ const resource = pathParts[pathParts.length - 1]?.replace(/^:/, "") || "resource";
2619
+ return `${action} ${resource}`;
1952
2620
  }
1953
- async function handleHistory(req, res, runtime) {
1954
- const service = runtime.getService("x402_payment");
1955
- if (!service || !service.isActive()) {
1956
- res.status(503).json({ error: "x402 service not active" });
1957
- return;
1958
- }
1959
- const limitStr = (Array.isArray(req.query?.limit) ? req.query.limit[0] : req.query?.limit) ?? "20";
1960
- const limit = Math.min(Math.max(parseInt(limitStr, 10) || 20, 1), 100);
1961
- const transactions = await service.getRecentTransactions(limit);
1962
- const serialized = transactions.map((txn) => ({
1963
- id: txn.id,
1964
- direction: txn.direction,
1965
- counterparty: txn.counterparty,
1966
- amount: txn.amount.toString(),
1967
- amountUsd: formatUsd(txn.amount),
1968
- network: txn.network,
1969
- txHash: txn.txHash,
1970
- resource: txn.resource,
1971
- status: txn.status,
1972
- createdAt: txn.createdAt,
1973
- metadata: txn.metadata
1974
- }));
1975
- res.status(200).json({ transactions: serialized, count: serialized.length });
1976
- }
1977
- async function handleExport(_req, res, runtime) {
1978
- const service = runtime.getService("x402_payment");
1979
- if (!service || !service.isActive()) {
1980
- res.status(503).json({ error: "x402 service not active" });
1981
- return;
2621
+ function applyPaymentProtection(routes, context) {
2622
+ if (!Array.isArray(routes)) {
2623
+ throw new Error("routes must be an array");
1982
2624
  }
1983
- const transactions = await service.getRecentTransactions(1e4);
1984
- const csvHeader = "id,direction,counterparty,amount_base_units,amount_usd,network,tx_hash,resource,status,created_at";
1985
- const csvRows = transactions.map((txn) => `${txn.id},${txn.direction},${txn.counterparty},${txn.amount.toString()},${formatUsd(txn.amount)},${txn.network},${txn.txHash},${escapeCSV(txn.resource)},${txn.status},${txn.createdAt}`);
1986
- const csv = [csvHeader, ...csvRows].join(`
2625
+ const validation = validateX402Startup(routes, context?.character, {
2626
+ agentId: context?.agentId
2627
+ });
2628
+ if (!validation.valid) {
2629
+ throw new Error(`
2630
+ x402 Configuration Invalid (${validation.errors.length} error${validation.errors.length > 1 ? "s" : ""}):
2631
+
2632
+ ` + validation.errors.map((e) => ` • ${e}`).join(`
2633
+ `) + `
2634
+
2635
+ Please fix these errors and try again.
1987
2636
  `);
1988
- if (res.setHeader) {
1989
- res.setHeader("Content-Type", "text/csv");
1990
- res.setHeader("Content-Disposition", `attachment; filename="x402-payments-${new Date().toISOString().slice(0, 10)}.csv"`);
1991
2637
  }
1992
- res.send(csv);
1993
- }
1994
- function escapeCSV(value) {
1995
- if (value.includes(",") || value.includes('"') || value.includes(`
1996
- `)) {
1997
- return `"${value.replace(/"/g, '""')}"`;
1998
- }
1999
- return value;
2638
+ return routes.map((route) => {
2639
+ const x402Route = route;
2640
+ if (x402Route.x402 != null) {
2641
+ if (isRoutePaymentWrapped(route)) {
2642
+ return route;
2643
+ }
2644
+ logger5.debug({ path: x402Route.path, x402: x402Route.x402 }, "[x402] payment protection enabled");
2645
+ const wrappedRoute = {
2646
+ ...route,
2647
+ handler: createPaymentAwareHandler(x402Route),
2648
+ [X402_ROUTE_PAYMENT_WRAPPED]: true
2649
+ };
2650
+ return wrappedRoute;
2651
+ }
2652
+ return route;
2653
+ });
2000
2654
  }
2655
+
2656
+ // src/index.ts
2001
2657
  var x402Plugin = {
2002
2658
  name: "x402",
2003
- description: "x402 HTTP payment protocol - send and receive crypto payments (USDC on EVM chains)",
2004
- config: {
2005
- X402_PRIVATE_KEY: null,
2006
- X402_NETWORK: null,
2007
- X402_PAY_TO: null,
2008
- X402_FACILITATOR_URL: null,
2009
- X402_MAX_PAYMENT_USD: null,
2010
- X402_MAX_TOTAL_USD: null,
2011
- X402_ENABLED: null
2012
- },
2013
- services: [X402Service],
2014
- actions: [payForServiceAction, checkPaymentHistoryAction, setPaymentPolicyAction],
2015
- providers: [paymentBalanceProvider],
2016
- routes: [
2017
- agentCardRoute,
2018
- {
2019
- type: "GET",
2020
- path: "/x402/summary",
2021
- name: "x402-summary",
2022
- public: false,
2023
- handler: handleSummary
2024
- },
2025
- {
2026
- type: "GET",
2027
- path: "/x402/history",
2028
- name: "x402-history",
2029
- public: false,
2030
- handler: handleHistory
2031
- },
2032
- {
2033
- type: "GET",
2034
- path: "/x402/export",
2035
- name: "x402-export",
2036
- public: false,
2037
- handler: handleExport
2038
- }
2039
- ]
2659
+ description: "x402 micropayment middleware for elizaOS plugin HTTP routes (HTTP 402 / payment-required).",
2660
+ actions: [],
2661
+ providers: [],
2662
+ evaluators: [],
2663
+ services: [],
2664
+ dispose: async (_runtime) => {}
2040
2665
  };
2041
- var typescript_default = x402Plugin;
2666
+ var src_default = x402Plugin;
2042
2667
  export {
2043
2668
  x402Plugin,
2044
- verifyPaymentWithFacilitator,
2045
- usdToBaseUnits,
2046
- truncateAddress,
2047
- settlePaymentWithFacilitator,
2048
- setPaymentPolicyAction,
2049
- resolveNetwork,
2050
- paymentBalanceProvider,
2051
- payForServiceAction,
2052
- networkKeyFromCaip2,
2053
- formatUsd,
2054
- typescript_default as default,
2055
- createPaywallMiddleware,
2056
- createFetchWithPayment,
2057
- checkPaymentHistoryAction,
2058
- agentCardRoute,
2059
- X402Service,
2060
- SqlitePaymentStorage,
2061
- PostgresPaymentStorage,
2062
- PolicyEngine,
2063
- ONE_DAY_MS,
2064
- NETWORK_REGISTRY,
2065
- MemoryPaymentStorage,
2066
- EvmPaymentSigner,
2067
- CircuitBreaker
2669
+ validateX402Startup,
2670
+ validateX402Response,
2671
+ validateAndThrowIfInvalid,
2672
+ validateAccepts,
2673
+ toX402Network,
2674
+ toResourceUrl,
2675
+ resolveEffectiveX402,
2676
+ registerX402Config,
2677
+ listX402Configs,
2678
+ isRoutePaymentWrapped,
2679
+ getX402Health,
2680
+ getPaymentConfig,
2681
+ getPaymentAddress,
2682
+ getBaseUrl,
2683
+ src_default as default,
2684
+ createX402Response,
2685
+ createPaymentAwareHandler,
2686
+ createAccepts,
2687
+ atomicAmountForPriceInCents,
2688
+ applyPaymentProtection,
2689
+ X402_ROUTE_PAYMENT_WRAPPED,
2690
+ X402_EVENT_PAYMENT_VERIFIED,
2691
+ X402_EVENT_PAYMENT_REQUIRED,
2692
+ PAYMENT_CONFIGS,
2693
+ PAYMENT_ADDRESSES,
2694
+ BUILT_IN_NETWORKS
2068
2695
  };
2069
2696
 
2070
- //# debugId=2EC8B924AB46A7D764756E2164756E21
2697
+ //# debugId=9C2A4CF3E2DCA87864756E2164756E21