@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.
Files changed (39) hide show
  1. package/dist/index.d.ts +57 -2
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +30919 -1913
  4. package/dist/index.js.map +114 -21
  5. package/dist/payment-config.d.ts +256 -0
  6. package/dist/payment-config.d.ts.map +1 -0
  7. package/dist/payment-wrapper.d.ts +42 -0
  8. package/dist/payment-wrapper.d.ts.map +1 -0
  9. package/dist/startup-validator.d.ts +28 -0
  10. package/dist/startup-validator.d.ts.map +1 -0
  11. package/dist/types.d.ts +158 -0
  12. package/dist/types.d.ts.map +1 -0
  13. package/dist/x402-facilitator-binding.d.ts +9 -0
  14. package/dist/x402-facilitator-binding.d.ts.map +1 -0
  15. package/dist/x402-replay-durable.d.ts +30 -0
  16. package/dist/x402-replay-durable.d.ts.map +1 -0
  17. package/dist/x402-replay-guard.d.ts +28 -0
  18. package/dist/x402-replay-guard.d.ts.map +1 -0
  19. package/dist/x402-replay-keys.d.ts +21 -0
  20. package/dist/x402-replay-keys.d.ts.map +1 -0
  21. package/dist/x402-resolve.d.ts +6 -0
  22. package/dist/x402-resolve.d.ts.map +1 -0
  23. package/dist/x402-standard-payment.d.ts +130 -0
  24. package/dist/x402-standard-payment.d.ts.map +1 -0
  25. package/dist/x402-types.d.ts +130 -0
  26. package/dist/x402-types.d.ts.map +1 -0
  27. package/package.json +43 -94
  28. package/src/index.ts +113 -0
  29. package/src/payment-config.ts +737 -0
  30. package/src/payment-wrapper.ts +1991 -0
  31. package/src/startup-validator.ts +349 -0
  32. package/src/types.ts +177 -0
  33. package/src/x402-facilitator-binding.ts +104 -0
  34. package/src/x402-replay-durable.ts +320 -0
  35. package/src/x402-replay-guard.ts +165 -0
  36. package/src/x402-replay-keys.ts +151 -0
  37. package/src/x402-resolve.ts +43 -0
  38. package/src/x402-standard-payment.ts +519 -0
  39. 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
+ }