@elizaos/plugin-x402 2.0.0-alpha.6 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +57 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30919 -1913
- package/dist/index.js.map +114 -21
- package/dist/payment-config.d.ts +256 -0
- package/dist/payment-config.d.ts.map +1 -0
- package/dist/payment-wrapper.d.ts +42 -0
- package/dist/payment-wrapper.d.ts.map +1 -0
- package/dist/startup-validator.d.ts +28 -0
- package/dist/startup-validator.d.ts.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/x402-facilitator-binding.d.ts +9 -0
- package/dist/x402-facilitator-binding.d.ts.map +1 -0
- package/dist/x402-replay-durable.d.ts +30 -0
- package/dist/x402-replay-durable.d.ts.map +1 -0
- package/dist/x402-replay-guard.d.ts +28 -0
- package/dist/x402-replay-guard.d.ts.map +1 -0
- package/dist/x402-replay-keys.d.ts +21 -0
- package/dist/x402-replay-keys.d.ts.map +1 -0
- package/dist/x402-resolve.d.ts +6 -0
- package/dist/x402-resolve.d.ts.map +1 -0
- package/dist/x402-standard-payment.d.ts +130 -0
- package/dist/x402-standard-payment.d.ts.map +1 -0
- package/dist/x402-types.d.ts +130 -0
- package/dist/x402-types.d.ts.map +1 -0
- package/package.json +43 -94
- package/src/index.ts +113 -0
- package/src/payment-config.ts +737 -0
- package/src/payment-wrapper.ts +1991 -0
- package/src/startup-validator.ts +349 -0
- package/src/types.ts +177 -0
- package/src/x402-facilitator-binding.ts +104 -0
- package/src/x402-replay-durable.ts +320 -0
- package/src/x402-replay-guard.ts +165 -0
- package/src/x402-replay-keys.ts +151 -0
- package/src/x402-resolve.ts +43 -0
- package/src/x402-standard-payment.ts +519 -0
- package/src/x402-types.ts +376 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for x402 micropayment system
|
|
3
|
+
* Route-specific pricing is now defined locally in each route definition
|
|
4
|
+
*
|
|
5
|
+
* Payment Verification Methods:
|
|
6
|
+
*
|
|
7
|
+
* 1. Direct Blockchain Proof (X-Payment-Proof header)
|
|
8
|
+
* - User sends payment transaction on-chain
|
|
9
|
+
* - Transaction signature is verified against blockchain
|
|
10
|
+
* - Supports: Solana, Base, Polygon
|
|
11
|
+
* - Format: base64-encoded JSON with signature and authorization
|
|
12
|
+
*
|
|
13
|
+
* 2. Facilitator Payment ID (X-Payment-Id header)
|
|
14
|
+
* - Third-party service handles payment
|
|
15
|
+
* - Service returns payment ID after successful payment
|
|
16
|
+
* - ID is verified through facilitator API
|
|
17
|
+
* - Configured via X402_FACILITATOR_URL environment variable
|
|
18
|
+
* - Example: X402_FACILITATOR_URL=https://facilitator.x402.ai
|
|
19
|
+
*
|
|
20
|
+
* 3. Standard X-Payment / PAYMENT-SIGNATURE (x402-fetch / CDP-style)
|
|
21
|
+
* - Base64(JSON) or raw JSON: `{ x402Version, accepted, payload }`
|
|
22
|
+
* - Verified and settled with POST `{ paymentPayload, paymentRequirements }`
|
|
23
|
+
* to facilitator `/verify` then `/settle`
|
|
24
|
+
* - Override endpoints with `X402_FACILITATOR_VERIFY_URL` and
|
|
25
|
+
* `X402_FACILITATOR_SETTLE_URL`; otherwise append `/verify` and `/settle`
|
|
26
|
+
* to `X402_FACILITATOR_URL`.
|
|
27
|
+
*
|
|
28
|
+
* The facilitator endpoint should implement:
|
|
29
|
+
* GET /verify/{paymentId}
|
|
30
|
+
* - 200 OK: Payment is valid (with optional { valid: true } JSON body)
|
|
31
|
+
* - 404 Not Found: Payment ID doesn't exist
|
|
32
|
+
* - 410 Gone: Payment already used (prevents replay attacks)
|
|
33
|
+
*
|
|
34
|
+
* Seller-side replay: proof / payment ID keys are atomically reserved in the
|
|
35
|
+
* SQL-backed runtime cache by default (`X402_REPLAY_DURABLE`, see x402 docs),
|
|
36
|
+
* then marked consumed after successful verification. Disable with
|
|
37
|
+
* `X402_REPLAY_DURABLE=0` for in-memory TTL-only behavior (dev / tests).
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { logger } from "@elizaos/core";
|
|
41
|
+
import type { X402ScanNetwork } from "./x402-types.js";
|
|
42
|
+
|
|
43
|
+
/** Networks supported by built-in x402 presets and verification */
|
|
44
|
+
export type Network = "BASE" | "SOLANA" | "POLYGON" | "BSC";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Built-in networks supported by default
|
|
48
|
+
*/
|
|
49
|
+
export const BUILT_IN_NETWORKS = ["BASE", "SOLANA", "POLYGON", "BSC"] as const;
|
|
50
|
+
|
|
51
|
+
// Default network configuration
|
|
52
|
+
export const DEFAULT_NETWORK: Network = "SOLANA";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert our Network type to x402scan-compliant network names
|
|
56
|
+
* @throws {Error} If network is not supported by x402scan
|
|
57
|
+
*/
|
|
58
|
+
export function toX402Network(network: Network): X402ScanNetwork {
|
|
59
|
+
const networkMap: Partial<Record<Network, X402ScanNetwork>> = {
|
|
60
|
+
BASE: "base",
|
|
61
|
+
SOLANA: "solana",
|
|
62
|
+
POLYGON: "polygon",
|
|
63
|
+
BSC: "bsc",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const mappedNetwork = networkMap[network];
|
|
67
|
+
if (!mappedNetwork) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Network '${network}' is not supported by x402scan. ` +
|
|
70
|
+
`Supported networks: ${BUILT_IN_NETWORKS.join(", ")}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return mappedNetwork;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Shipped fallbacks — not your treasury; startup validation warns / errors in production. */
|
|
78
|
+
export const BUNDLED_EXAMPLE_EVM_PAYOUT =
|
|
79
|
+
"0x066E94e1200aa765d0A6392777D543Aa6Dea606C";
|
|
80
|
+
export const BUNDLED_EXAMPLE_SOLANA_PAYOUT =
|
|
81
|
+
"3nMBmufBUBVnk28sTp3NsrSJsdVGTyLZYmsqpMFaUT9J";
|
|
82
|
+
|
|
83
|
+
export function paymentAddressIsBundledExample(
|
|
84
|
+
network: Network,
|
|
85
|
+
paymentAddress: string,
|
|
86
|
+
): boolean {
|
|
87
|
+
const a = paymentAddress.trim();
|
|
88
|
+
if (!a) return false;
|
|
89
|
+
if (network === "SOLANA") return a === BUNDLED_EXAMPLE_SOLANA_PAYOUT;
|
|
90
|
+
if (network === "BASE" || network === "POLYGON" || network === "BSC") {
|
|
91
|
+
return a.toLowerCase() === BUNDLED_EXAMPLE_EVM_PAYOUT.toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Network-specific wallet addresses
|
|
98
|
+
* Uses existing environment variables from your project configuration
|
|
99
|
+
*/
|
|
100
|
+
export const PAYMENT_ADDRESSES: Partial<Record<Network, string>> = {
|
|
101
|
+
BASE:
|
|
102
|
+
process.env.BASE_PUBLIC_KEY ||
|
|
103
|
+
process.env.PAYMENT_WALLET_BASE ||
|
|
104
|
+
BUNDLED_EXAMPLE_EVM_PAYOUT,
|
|
105
|
+
SOLANA:
|
|
106
|
+
process.env.SOLANA_PUBLIC_KEY ||
|
|
107
|
+
process.env.PAYMENT_WALLET_SOLANA ||
|
|
108
|
+
BUNDLED_EXAMPLE_SOLANA_PAYOUT,
|
|
109
|
+
POLYGON:
|
|
110
|
+
process.env.POLYGON_PUBLIC_KEY || process.env.PAYMENT_WALLET_POLYGON || "",
|
|
111
|
+
BSC:
|
|
112
|
+
process.env.BSC_PUBLIC_KEY ||
|
|
113
|
+
process.env.PAYMENT_WALLET_BSC ||
|
|
114
|
+
BUNDLED_EXAMPLE_EVM_PAYOUT,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the base URL for the current server
|
|
119
|
+
* Used to construct full resource URLs for x402 responses
|
|
120
|
+
*/
|
|
121
|
+
export function getBaseUrl(): string {
|
|
122
|
+
// Check for explicit base URL setting
|
|
123
|
+
if (process.env.X402_BASE_URL) {
|
|
124
|
+
return process.env.X402_BASE_URL.replace(/\/$/, ""); // Remove trailing slash
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return "https://x402.elizacloud.ai";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Convert a route path to a full resource URL
|
|
132
|
+
*/
|
|
133
|
+
export function toResourceUrl(path: string): string {
|
|
134
|
+
const baseUrl = getBaseUrl();
|
|
135
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
136
|
+
return `${baseUrl}${cleanPath}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Token configuration for Solana
|
|
141
|
+
*/
|
|
142
|
+
export const SOLANA_TOKENS = {
|
|
143
|
+
USDC: {
|
|
144
|
+
symbol: "USDC",
|
|
145
|
+
address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
146
|
+
decimals: 6,
|
|
147
|
+
},
|
|
148
|
+
AI16Z: {
|
|
149
|
+
symbol: "ai16z",
|
|
150
|
+
address: "HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC",
|
|
151
|
+
decimals: 6,
|
|
152
|
+
},
|
|
153
|
+
DEGENAI: {
|
|
154
|
+
symbol: "degenai",
|
|
155
|
+
address: "Gu3LDkn7Vx3bmCzLafYNKcDxv2mH7YN44NJZFXnypump",
|
|
156
|
+
decimals: 6,
|
|
157
|
+
},
|
|
158
|
+
ELIZAOS: {
|
|
159
|
+
symbol: "elizaOS",
|
|
160
|
+
address: "DuMbhu7mvQvqQHGcnikDgb4XegXJRyhUBfdU22uELiZA",
|
|
161
|
+
decimals: 6,
|
|
162
|
+
},
|
|
163
|
+
} as const;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Token configuration for Base (EVM)
|
|
167
|
+
*/
|
|
168
|
+
export const BASE_TOKENS = {
|
|
169
|
+
USDC: {
|
|
170
|
+
symbol: "USDC",
|
|
171
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
172
|
+
decimals: 6,
|
|
173
|
+
},
|
|
174
|
+
ELIZAOS: {
|
|
175
|
+
symbol: "elizaOS",
|
|
176
|
+
address: "0xea17Df5Cf6D172224892B5477A16ACb111182478",
|
|
177
|
+
decimals: 18,
|
|
178
|
+
},
|
|
179
|
+
} as const;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Token configuration for Polygon (EVM)
|
|
183
|
+
*/
|
|
184
|
+
export const POLYGON_TOKENS = {
|
|
185
|
+
USDC: {
|
|
186
|
+
symbol: "USDC",
|
|
187
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
188
|
+
decimals: 6,
|
|
189
|
+
},
|
|
190
|
+
} as const;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Token configuration for BNB Smart Chain (EVM)
|
|
194
|
+
*/
|
|
195
|
+
export const BSC_TOKENS = {
|
|
196
|
+
USDC: {
|
|
197
|
+
symbol: "USDC",
|
|
198
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
199
|
+
decimals: 18,
|
|
200
|
+
},
|
|
201
|
+
} as const;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Default asset for each network (used in x402 responses)
|
|
205
|
+
*/
|
|
206
|
+
export const NETWORK_ASSETS: Partial<Record<Network, string>> = {
|
|
207
|
+
BASE: "USDC", // USDC on Base
|
|
208
|
+
SOLANA: "USDC", // USDC on Solana (default, but also supports ai16z and degenai)
|
|
209
|
+
POLYGON: "USDC", // USDC on Polygon
|
|
210
|
+
BSC: "USDC", // Binance-Peg USDC on BNB Smart Chain
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get all accepted assets for a network
|
|
215
|
+
* @throws {Error} If network is not supported
|
|
216
|
+
*/
|
|
217
|
+
export function getNetworkAssets(network: Network): string[] {
|
|
218
|
+
if (network === "SOLANA") {
|
|
219
|
+
return Object.values(SOLANA_TOKENS).map((t) => t.symbol);
|
|
220
|
+
}
|
|
221
|
+
if (network === "BASE") {
|
|
222
|
+
return Object.values(BASE_TOKENS).map((t) => t.symbol);
|
|
223
|
+
}
|
|
224
|
+
if (network === "POLYGON") {
|
|
225
|
+
return Object.values(POLYGON_TOKENS).map((t) => t.symbol);
|
|
226
|
+
}
|
|
227
|
+
if (network === "BSC") {
|
|
228
|
+
return Object.values(BSC_TOKENS).map((t) => t.symbol);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const defaultAsset = NETWORK_ASSETS[network];
|
|
232
|
+
if (!defaultAsset) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Network '${network}' is not configured. ` +
|
|
235
|
+
`Supported networks: ${BUILT_IN_NETWORKS.join(", ")}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return [defaultAsset];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Default/legacy wallet address (uses default network)
|
|
243
|
+
export const PAYMENT_RECEIVER_ADDRESS =
|
|
244
|
+
PAYMENT_ADDRESSES[DEFAULT_NETWORK] || "";
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Named payment config definition - stores individual fields, CAIP-19 constructed on-demand
|
|
248
|
+
*/
|
|
249
|
+
export interface PaymentConfigDefinition {
|
|
250
|
+
network: Network;
|
|
251
|
+
assetNamespace: string; // e.g., "erc20", "spl-token", "slip44"
|
|
252
|
+
assetReference: string; // e.g., contract address or token mint
|
|
253
|
+
paymentAddress: string; // Recipient address
|
|
254
|
+
symbol: string; // Display symbol (USDC, ETH, etc.)
|
|
255
|
+
chainId?: string; // Optional chain ID for CAIP-2 (e.g., "8453" for Base)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Payment configuration registry - named configs for easy reference
|
|
260
|
+
*/
|
|
261
|
+
export const PAYMENT_CONFIGS: Record<string, PaymentConfigDefinition> = {
|
|
262
|
+
base_usdc: {
|
|
263
|
+
network: "BASE",
|
|
264
|
+
assetNamespace: "erc20",
|
|
265
|
+
assetReference: BASE_TOKENS.USDC.address,
|
|
266
|
+
paymentAddress: PAYMENT_ADDRESSES.BASE ?? BUNDLED_EXAMPLE_EVM_PAYOUT,
|
|
267
|
+
symbol: "USDC",
|
|
268
|
+
chainId: "8453",
|
|
269
|
+
},
|
|
270
|
+
solana_usdc: {
|
|
271
|
+
network: "SOLANA",
|
|
272
|
+
assetNamespace: "spl-token",
|
|
273
|
+
assetReference: SOLANA_TOKENS.USDC.address,
|
|
274
|
+
paymentAddress: PAYMENT_ADDRESSES.SOLANA ?? BUNDLED_EXAMPLE_SOLANA_PAYOUT,
|
|
275
|
+
symbol: "USDC",
|
|
276
|
+
},
|
|
277
|
+
polygon_usdc: {
|
|
278
|
+
network: "POLYGON",
|
|
279
|
+
assetNamespace: "erc20",
|
|
280
|
+
assetReference: POLYGON_TOKENS.USDC.address,
|
|
281
|
+
paymentAddress: PAYMENT_ADDRESSES.POLYGON || "",
|
|
282
|
+
symbol: "USDC",
|
|
283
|
+
chainId: "137",
|
|
284
|
+
},
|
|
285
|
+
bsc_usdc: {
|
|
286
|
+
network: "BSC",
|
|
287
|
+
assetNamespace: "erc20",
|
|
288
|
+
assetReference: BSC_TOKENS.USDC.address,
|
|
289
|
+
paymentAddress: PAYMENT_ADDRESSES.BSC ?? BUNDLED_EXAMPLE_EVM_PAYOUT,
|
|
290
|
+
symbol: "USDC",
|
|
291
|
+
chainId: "56",
|
|
292
|
+
},
|
|
293
|
+
base_elizaos: {
|
|
294
|
+
network: "BASE",
|
|
295
|
+
assetNamespace: "erc20",
|
|
296
|
+
assetReference: BASE_TOKENS.ELIZAOS.address,
|
|
297
|
+
paymentAddress: PAYMENT_ADDRESSES.BASE ?? BUNDLED_EXAMPLE_EVM_PAYOUT,
|
|
298
|
+
symbol: "elizaOS",
|
|
299
|
+
chainId: "8453",
|
|
300
|
+
},
|
|
301
|
+
solana_elizaos: {
|
|
302
|
+
network: "SOLANA",
|
|
303
|
+
assetNamespace: "spl-token",
|
|
304
|
+
assetReference: SOLANA_TOKENS.ELIZAOS.address,
|
|
305
|
+
paymentAddress: PAYMENT_ADDRESSES.SOLANA ?? BUNDLED_EXAMPLE_SOLANA_PAYOUT,
|
|
306
|
+
symbol: "elizaOS",
|
|
307
|
+
},
|
|
308
|
+
solana_degenai: {
|
|
309
|
+
network: "SOLANA",
|
|
310
|
+
assetNamespace: "spl-token",
|
|
311
|
+
assetReference: SOLANA_TOKENS.DEGENAI.address,
|
|
312
|
+
paymentAddress: PAYMENT_ADDRESSES.SOLANA ?? BUNDLED_EXAMPLE_SOLANA_PAYOUT,
|
|
313
|
+
symbol: "degenai",
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Construct CAIP-19 asset ID from payment config fields
|
|
319
|
+
*/
|
|
320
|
+
export function getCAIP19FromConfig(config: PaymentConfigDefinition): string {
|
|
321
|
+
// Build CAIP-2 chain ID: namespace:reference
|
|
322
|
+
const chainNamespace = config.network === "SOLANA" ? "solana" : "eip155";
|
|
323
|
+
const chainReference =
|
|
324
|
+
config.chainId ||
|
|
325
|
+
(config.network === "BASE"
|
|
326
|
+
? "8453"
|
|
327
|
+
: config.network === "POLYGON"
|
|
328
|
+
? "137"
|
|
329
|
+
: config.network === "BSC"
|
|
330
|
+
? "56"
|
|
331
|
+
: "1");
|
|
332
|
+
const chainId = `${chainNamespace}:${chainReference}`;
|
|
333
|
+
|
|
334
|
+
// Build asset part: namespace:reference
|
|
335
|
+
const assetId = `${config.assetNamespace}:${config.assetReference}`;
|
|
336
|
+
|
|
337
|
+
// Full CAIP-19: chain_id/asset_namespace:asset_reference
|
|
338
|
+
return `${chainId}/${assetId}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Mutable registry for custom payment configs
|
|
343
|
+
* Plugins can register configs via registerX402Config()
|
|
344
|
+
*/
|
|
345
|
+
const CUSTOM_PAYMENT_CONFIGS: Record<string, PaymentConfigDefinition> = {};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Register a custom payment configuration
|
|
349
|
+
* Plugins call this in their init() function
|
|
350
|
+
*
|
|
351
|
+
* A second call with the same `name` (or the same `agentId`+`name` for scoped
|
|
352
|
+
* keys) throws unless `override: true` is set, so two plugins cannot silently
|
|
353
|
+
* replace each other in `CUSTOM_PAYMENT_CONFIGS`.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```typescript
|
|
357
|
+
* registerX402Config('base_ai16z', {
|
|
358
|
+
* network: 'BASE',
|
|
359
|
+
* assetNamespace: 'erc20',
|
|
360
|
+
* assetReference: '0x...',
|
|
361
|
+
* paymentAddress: process.env.BASE_PUBLIC_KEY,
|
|
362
|
+
* symbol: 'AI16Z',
|
|
363
|
+
* chainId: '8453'
|
|
364
|
+
* });
|
|
365
|
+
*
|
|
366
|
+
* // Agent-specific override
|
|
367
|
+
* registerX402Config('base_usdc', {...}, { agentId: runtime.agentId });
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
export function registerX402Config(
|
|
371
|
+
name: string,
|
|
372
|
+
config: PaymentConfigDefinition,
|
|
373
|
+
options?: { override?: boolean; agentId?: string },
|
|
374
|
+
): void {
|
|
375
|
+
// Prevent accidental override of built-in configs
|
|
376
|
+
if (PAYMENT_CONFIGS[name] && !options?.override) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`Payment config '${name}' already exists. Use override: true to replace it.`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const registryKey = options?.agentId ? `${options.agentId}:${name}` : name;
|
|
383
|
+
if (CUSTOM_PAYMENT_CONFIGS[registryKey] && !options?.override) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`Payment config '${registryKey}' is already registered. Use override: true to replace it.`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
CUSTOM_PAYMENT_CONFIGS[registryKey] = config;
|
|
390
|
+
|
|
391
|
+
logger.debug(
|
|
392
|
+
{ registryKey, symbol: config.symbol, network: config.network },
|
|
393
|
+
"[x402] registered payment config",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get payment config - checks custom registry then built-in
|
|
399
|
+
* Supports agent-specific configs via agentId parameter
|
|
400
|
+
*/
|
|
401
|
+
export function getPaymentConfig(
|
|
402
|
+
name: string,
|
|
403
|
+
agentId?: string,
|
|
404
|
+
): PaymentConfigDefinition {
|
|
405
|
+
// Check agent-specific config first
|
|
406
|
+
if (agentId) {
|
|
407
|
+
const agentConfig = CUSTOM_PAYMENT_CONFIGS[`${agentId}:${name}`];
|
|
408
|
+
if (agentConfig) return agentConfig;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Check custom global configs
|
|
412
|
+
const customConfig = CUSTOM_PAYMENT_CONFIGS[name];
|
|
413
|
+
if (customConfig) return customConfig;
|
|
414
|
+
|
|
415
|
+
// Check built-in configs
|
|
416
|
+
const builtInConfig = PAYMENT_CONFIGS[name];
|
|
417
|
+
if (!builtInConfig) {
|
|
418
|
+
const available = [
|
|
419
|
+
...Object.keys(PAYMENT_CONFIGS),
|
|
420
|
+
...Object.keys(CUSTOM_PAYMENT_CONFIGS).filter((k) => !k.includes(":")),
|
|
421
|
+
];
|
|
422
|
+
throw new Error(
|
|
423
|
+
`Unknown payment config '${name}'. Available: ${available.join(", ")}`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return builtInConfig;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* List all available payment configs (built-in + custom)
|
|
431
|
+
* Optionally filter to agent-specific configs
|
|
432
|
+
*/
|
|
433
|
+
export function listX402Configs(agentId?: string): string[] {
|
|
434
|
+
const configs = new Set([
|
|
435
|
+
...Object.keys(PAYMENT_CONFIGS),
|
|
436
|
+
...Object.keys(CUSTOM_PAYMENT_CONFIGS).filter((k) => !k.includes(":")),
|
|
437
|
+
]);
|
|
438
|
+
|
|
439
|
+
if (agentId) {
|
|
440
|
+
for (const k of Object.keys(CUSTOM_PAYMENT_CONFIGS)) {
|
|
441
|
+
if (k.startsWith(`${agentId}:`)) {
|
|
442
|
+
const short = k.split(":")[1];
|
|
443
|
+
if (short) configs.add(short);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return Array.from(configs).sort();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Validate payment config name
|
|
453
|
+
*/
|
|
454
|
+
export function validatePaymentConfigName(name: string): boolean {
|
|
455
|
+
return name in PAYMENT_CONFIGS;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Re-export X402Config from core for convenience
|
|
459
|
+
export type { X402Config } from "@elizaos/core";
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get the payment address for a specific network
|
|
463
|
+
* @throws {Error} If network is not configured
|
|
464
|
+
*/
|
|
465
|
+
export function getPaymentAddress(network: Network): string {
|
|
466
|
+
const address = PAYMENT_ADDRESSES[network];
|
|
467
|
+
if (!address) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`No payment address configured for network '${network}'. ` +
|
|
470
|
+
`Supported networks: ${BUILT_IN_NETWORKS.join(", ")}. ` +
|
|
471
|
+
`Set ${network}_PUBLIC_KEY in your environment.`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return address;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get all network addresses with metadata
|
|
479
|
+
* Only returns networks that have configured addresses
|
|
480
|
+
*/
|
|
481
|
+
export function getNetworkAddresses(networks: Network[]): Array<{
|
|
482
|
+
name: Network;
|
|
483
|
+
address: string;
|
|
484
|
+
facilitatorEndpoint?: string;
|
|
485
|
+
}> {
|
|
486
|
+
return networks
|
|
487
|
+
.filter(
|
|
488
|
+
(network) =>
|
|
489
|
+
PAYMENT_ADDRESSES[network] !== undefined &&
|
|
490
|
+
PAYMENT_ADDRESSES[network] !== "",
|
|
491
|
+
)
|
|
492
|
+
.map((network) => ({
|
|
493
|
+
name: network,
|
|
494
|
+
address: PAYMENT_ADDRESSES[network] as string,
|
|
495
|
+
// Add facilitator endpoint for EVM chains if configured
|
|
496
|
+
...((network === "BASE" || network === "POLYGON" || network === "BSC") &&
|
|
497
|
+
process.env.EVM_FACILITATOR && {
|
|
498
|
+
facilitatorEndpoint: process.env.EVM_FACILITATOR,
|
|
499
|
+
}),
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Approximate USD/token map (float) for dashboards or legacy callers.
|
|
505
|
+
* **Atomic amounts** use exact rational math from the same env defaults — see
|
|
506
|
+
* `atomicAmountForPriceInCents` / `getTokenUsdPerTokenRational` in this file.
|
|
507
|
+
*/
|
|
508
|
+
export const TOKEN_PRICES_USD: Record<string, number> = {
|
|
509
|
+
USDC: 1.0,
|
|
510
|
+
ai16z: Number.parseFloat(process.env.AI16Z_PRICE_USD || "0.5"),
|
|
511
|
+
degenai: Number.parseFloat(process.env.DEGENAI_PRICE_USD || "0.01"),
|
|
512
|
+
elizaOS: Number.parseFloat(process.env.ELIZAOS_PRICE_USD || "0.05"),
|
|
513
|
+
ETH: 2000.0, // Simplified; override via env/oracle in future
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Get token decimals for an asset
|
|
518
|
+
*/
|
|
519
|
+
/**
|
|
520
|
+
* Parse a positive USD decimal string (e.g. "1.25", optional leading "$")
|
|
521
|
+
* into an exact positive rational num/den in dollars (not cents).
|
|
522
|
+
*/
|
|
523
|
+
function usdDecimalStringToRational(raw: string): { num: bigint; den: bigint } {
|
|
524
|
+
const s = raw.replace(/^\$/, "").trim();
|
|
525
|
+
if (!/^\d+(\.\d+)?$/.test(s)) {
|
|
526
|
+
throw new Error(`Invalid USD decimal: ${raw}`);
|
|
527
|
+
}
|
|
528
|
+
const [wi, fr = ""] = s.split(".");
|
|
529
|
+
const den = 10n ** BigInt(fr.length);
|
|
530
|
+
const whole = BigInt(wi || "0");
|
|
531
|
+
const frac = fr ? BigInt(fr) : 0n;
|
|
532
|
+
const num = whole * den + frac;
|
|
533
|
+
if (num <= 0n) {
|
|
534
|
+
throw new Error(`USD amount must be positive: ${raw}`);
|
|
535
|
+
}
|
|
536
|
+
return { num, den };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function envUsdPerTokenRational(
|
|
540
|
+
envKey: string,
|
|
541
|
+
fallback: string,
|
|
542
|
+
): { num: bigint; den: bigint } {
|
|
543
|
+
const v = process.env[envKey]?.trim();
|
|
544
|
+
return usdDecimalStringToRational(v && v.length > 0 ? v : fallback);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** USD (not cents) per 1 full token, as exact rational num/den */
|
|
548
|
+
function getTokenUsdPerTokenRational(
|
|
549
|
+
asset: string,
|
|
550
|
+
_network?: Network,
|
|
551
|
+
): { num: bigint; den: bigint } {
|
|
552
|
+
const upper = asset.toUpperCase();
|
|
553
|
+
if (upper === "USDC") return { num: 1n, den: 1n };
|
|
554
|
+
if (asset === "elizaOS" || upper === "ELIZAOS") {
|
|
555
|
+
return envUsdPerTokenRational("ELIZAOS_PRICE_USD", "0.05");
|
|
556
|
+
}
|
|
557
|
+
if (upper === "DEGENAI" || asset === "degenai") {
|
|
558
|
+
return envUsdPerTokenRational("DEGENAI_PRICE_USD", "0.01");
|
|
559
|
+
}
|
|
560
|
+
if (upper === "AI16Z" || asset === "ai16z") {
|
|
561
|
+
return envUsdPerTokenRational("AI16Z_PRICE_USD", "0.5");
|
|
562
|
+
}
|
|
563
|
+
if (upper === "ETH") return { num: 2000n, den: 1n };
|
|
564
|
+
return { num: 1n, den: 1n };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function getTokenDecimals(asset: string, network?: Network): number {
|
|
568
|
+
// Check network-specific tokens if network is provided
|
|
569
|
+
if (network === "SOLANA") {
|
|
570
|
+
const solanaToken = Object.values(SOLANA_TOKENS).find(
|
|
571
|
+
(t) => t.symbol === asset,
|
|
572
|
+
);
|
|
573
|
+
if (solanaToken) return solanaToken.decimals;
|
|
574
|
+
}
|
|
575
|
+
if (network === "BASE") {
|
|
576
|
+
const baseToken = Object.values(BASE_TOKENS).find(
|
|
577
|
+
(t) => t.symbol === asset,
|
|
578
|
+
);
|
|
579
|
+
if (baseToken) return baseToken.decimals;
|
|
580
|
+
}
|
|
581
|
+
if (network === "POLYGON") {
|
|
582
|
+
const polygonToken = Object.values(POLYGON_TOKENS).find(
|
|
583
|
+
(t) => t.symbol === asset,
|
|
584
|
+
);
|
|
585
|
+
if (polygonToken) return polygonToken.decimals;
|
|
586
|
+
}
|
|
587
|
+
if (network === "BSC") {
|
|
588
|
+
const bscToken = Object.values(BSC_TOKENS).find((t) => t.symbol === asset);
|
|
589
|
+
if (bscToken) return bscToken.decimals;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Check all token configs if no network specified
|
|
593
|
+
const solanaToken = Object.values(SOLANA_TOKENS).find(
|
|
594
|
+
(t) => t.symbol === asset,
|
|
595
|
+
);
|
|
596
|
+
if (solanaToken) return solanaToken.decimals;
|
|
597
|
+
|
|
598
|
+
const baseToken = Object.values(BASE_TOKENS).find((t) => t.symbol === asset);
|
|
599
|
+
if (baseToken) return baseToken.decimals;
|
|
600
|
+
|
|
601
|
+
const polygonToken = Object.values(POLYGON_TOKENS).find(
|
|
602
|
+
(t) => t.symbol === asset,
|
|
603
|
+
);
|
|
604
|
+
if (polygonToken) return polygonToken.decimals;
|
|
605
|
+
|
|
606
|
+
const bscToken = Object.values(BSC_TOKENS).find((t) => t.symbol === asset);
|
|
607
|
+
if (bscToken) return bscToken.decimals;
|
|
608
|
+
|
|
609
|
+
// Defaults
|
|
610
|
+
if (asset === "USDC") return 6;
|
|
611
|
+
if (asset === "ETH") return 18;
|
|
612
|
+
|
|
613
|
+
return 6; // Default to 6 decimals
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Smallest-unit token amount for x402 `maxAmountRequired` and verification,
|
|
618
|
+
* from integer USD cents and a concrete payment config (symbol + network).
|
|
619
|
+
*/
|
|
620
|
+
export function atomicAmountForPriceInCents(
|
|
621
|
+
priceInCents: number,
|
|
622
|
+
config: PaymentConfigDefinition,
|
|
623
|
+
): string {
|
|
624
|
+
if (!Number.isFinite(priceInCents) || priceInCents <= 0) {
|
|
625
|
+
throw new Error("priceInCents must be a positive finite number");
|
|
626
|
+
}
|
|
627
|
+
const cents = BigInt(Math.floor(priceInCents));
|
|
628
|
+
const { num: p, den: q } = getTokenUsdPerTokenRational(
|
|
629
|
+
config.symbol,
|
|
630
|
+
config.network,
|
|
631
|
+
);
|
|
632
|
+
const dec = getTokenDecimals(config.symbol, config.network);
|
|
633
|
+
if (dec < 0 || dec > 120) {
|
|
634
|
+
throw new Error("invalid token decimals for payment config");
|
|
635
|
+
}
|
|
636
|
+
const scale = 10n ** BigInt(dec);
|
|
637
|
+
const numer = cents * q * scale;
|
|
638
|
+
const denom = 100n * p;
|
|
639
|
+
if (denom === 0n) {
|
|
640
|
+
throw new Error("invalid token USD price (zero denominator)");
|
|
641
|
+
}
|
|
642
|
+
return ((numer + denom - 1n) / denom).toString();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Parse price string (e.g., "$0.10") as a USD **dollar** amount and convert to
|
|
647
|
+
* the asset’s smallest units (ceil), using the same rational pricing as
|
|
648
|
+
* `atomicAmountForPriceInCents` / env overrides (`ELIZAOS_PRICE_USD`, etc.).
|
|
649
|
+
*/
|
|
650
|
+
export function parsePrice(
|
|
651
|
+
price: string,
|
|
652
|
+
asset: string = "USDC",
|
|
653
|
+
network?: Network,
|
|
654
|
+
): string {
|
|
655
|
+
const { num: un, den: ud } = usdDecimalStringToRational(price);
|
|
656
|
+
const { num: p, den: q } = getTokenUsdPerTokenRational(asset, network);
|
|
657
|
+
const dec = getTokenDecimals(asset, network);
|
|
658
|
+
if (dec < 0 || dec > 120) {
|
|
659
|
+
throw new Error("invalid token decimals");
|
|
660
|
+
}
|
|
661
|
+
const scale = 10n ** BigInt(dec);
|
|
662
|
+
const numer = un * q * scale;
|
|
663
|
+
const denom = ud * p;
|
|
664
|
+
if (denom === 0n) {
|
|
665
|
+
throw new Error("invalid token USD price (zero denominator)");
|
|
666
|
+
}
|
|
667
|
+
return ((numer + denom - 1n) / denom).toString();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Get token address for any network and asset
|
|
672
|
+
*/
|
|
673
|
+
export function getTokenAddress(
|
|
674
|
+
asset: string,
|
|
675
|
+
network: Network,
|
|
676
|
+
): string | undefined {
|
|
677
|
+
if (network === "SOLANA") {
|
|
678
|
+
const token = Object.values(SOLANA_TOKENS).find((t) => t.symbol === asset);
|
|
679
|
+
return token?.address;
|
|
680
|
+
}
|
|
681
|
+
if (network === "BASE") {
|
|
682
|
+
const token = Object.values(BASE_TOKENS).find((t) => t.symbol === asset);
|
|
683
|
+
return token?.address;
|
|
684
|
+
}
|
|
685
|
+
if (network === "POLYGON") {
|
|
686
|
+
const token = Object.values(POLYGON_TOKENS).find((t) => t.symbol === asset);
|
|
687
|
+
return token?.address;
|
|
688
|
+
}
|
|
689
|
+
if (network === "BSC") {
|
|
690
|
+
const token = Object.values(BSC_TOKENS).find((t) => t.symbol === asset);
|
|
691
|
+
return token?.address;
|
|
692
|
+
}
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get the asset for a specific network
|
|
698
|
+
* @throws {Error} If network is not configured
|
|
699
|
+
*/
|
|
700
|
+
export function getNetworkAsset(network: Network): string {
|
|
701
|
+
const asset = NETWORK_ASSETS[network];
|
|
702
|
+
if (!asset) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`No default asset configured for network '${network}'. ` +
|
|
705
|
+
`Supported networks: ${BUILT_IN_NETWORKS.join(", ")}`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return asset;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Get x402 system health status
|
|
713
|
+
* Useful for monitoring and debugging
|
|
714
|
+
*/
|
|
715
|
+
export function getX402Health(): {
|
|
716
|
+
networks: Array<{
|
|
717
|
+
network: Network;
|
|
718
|
+
configured: boolean;
|
|
719
|
+
address: string | null;
|
|
720
|
+
}>;
|
|
721
|
+
facilitator: { url: string | null; configured: boolean };
|
|
722
|
+
} {
|
|
723
|
+
const networks: Network[] = ["BASE", "SOLANA", "POLYGON", "BSC"];
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
networks: networks.map((network) => ({
|
|
727
|
+
network,
|
|
728
|
+
configured:
|
|
729
|
+
!!PAYMENT_ADDRESSES[network] && PAYMENT_ADDRESSES[network] !== "",
|
|
730
|
+
address: PAYMENT_ADDRESSES[network] || null,
|
|
731
|
+
})),
|
|
732
|
+
facilitator: {
|
|
733
|
+
url: process.env.X402_FACILITATOR_URL || null,
|
|
734
|
+
configured: !!process.env.X402_FACILITATOR_URL,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|