@dexterai/x402 1.7.2 → 1.8.0

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.
@@ -40,25 +40,77 @@ function decodeBase64Json(encoded) {
40
40
  }
41
41
 
42
42
  // src/server/facilitator-client.ts
43
+ function isRetryable(error) {
44
+ if (error instanceof TypeError) return true;
45
+ if (error && typeof error === "object" && "status" in error) {
46
+ const status = error.status;
47
+ return status >= 500 && status < 600;
48
+ }
49
+ return false;
50
+ }
51
+ var HttpError = class extends Error {
52
+ status;
53
+ body;
54
+ constructor(status, body) {
55
+ super(`HTTP ${status}`);
56
+ this.status = status;
57
+ this.body = body;
58
+ }
59
+ };
43
60
  var FacilitatorClient = class {
44
61
  facilitatorUrl;
45
62
  cachedSupported = null;
46
63
  cacheTime = 0;
47
64
  CACHE_TTL_MS = 6e4;
48
- // 1 minute cache
49
- constructor(facilitatorUrl = DEXTER_FACILITATOR_URL) {
65
+ timeoutMs;
66
+ maxRetries;
67
+ retryBaseMs;
68
+ constructor(facilitatorUrl = DEXTER_FACILITATOR_URL, config) {
50
69
  this.facilitatorUrl = facilitatorUrl.replace(/\/$/, "");
70
+ this.timeoutMs = config?.timeoutMs ?? 1e4;
71
+ this.maxRetries = config?.maxRetries ?? 3;
72
+ this.retryBaseMs = config?.retryBaseMs ?? 500;
73
+ }
74
+ async fetchWithTimeout(url, init) {
75
+ const controller = new AbortController();
76
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
77
+ try {
78
+ return await fetch(url, { ...init, signal: controller.signal });
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+ async fetchWithRetry(url, init) {
84
+ let lastError;
85
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
86
+ try {
87
+ const response = await this.fetchWithTimeout(url, init);
88
+ if (!response.ok && response.status >= 500) {
89
+ throw new HttpError(response.status, await response.text());
90
+ }
91
+ return response;
92
+ } catch (error) {
93
+ lastError = error;
94
+ if (attempt < this.maxRetries - 1 && isRetryable(error)) {
95
+ const delay = this.retryBaseMs * Math.pow(2, attempt);
96
+ await new Promise((r) => setTimeout(r, delay));
97
+ continue;
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+ throw lastError;
51
103
  }
52
104
  /**
53
- * Get supported payment kinds from the facilitator
54
- * Results are cached for 1 minute to reduce network calls
105
+ * Get supported payment kinds from the facilitator.
106
+ * Results are cached for 1 minute to reduce network calls.
55
107
  */
56
108
  async getSupported() {
57
109
  const now = Date.now();
58
110
  if (this.cachedSupported && now - this.cacheTime < this.CACHE_TTL_MS) {
59
111
  return this.cachedSupported;
60
112
  }
61
- const response = await fetch(`${this.facilitatorUrl}/supported`);
113
+ const response = await this.fetchWithTimeout(`${this.facilitatorUrl}/supported`);
62
114
  if (!response.ok) {
63
115
  throw new Error(`Facilitator /supported returned ${response.status}`);
64
116
  }
@@ -68,9 +120,6 @@ var FacilitatorClient = class {
68
120
  }
69
121
  /**
70
122
  * Get the fee payer address for a specific network
71
- *
72
- * @param network - CAIP-2 network identifier
73
- * @returns Fee payer address
74
123
  */
75
124
  async getFeePayer(network) {
76
125
  const supported = await this.getSupported();
@@ -86,9 +135,6 @@ var FacilitatorClient = class {
86
135
  }
87
136
  /**
88
137
  * Get extra data for a network (feePayer, decimals, EIP-712 data, etc.)
89
- *
90
- * @param network - CAIP-2 network identifier
91
- * @returns Extra data from /supported
92
138
  */
93
139
  async getNetworkExtra(network) {
94
140
  const supported = await this.getSupported();
@@ -98,30 +144,22 @@ var FacilitatorClient = class {
98
144
  return kind?.extra;
99
145
  }
100
146
  /**
101
- * Verify a payment with the facilitator
102
- *
103
- * @param paymentSignatureHeader - Base64-encoded PAYMENT-SIGNATURE header value
104
- * @param requirements - The payment requirements that were sent to the client
105
- * @returns Verification response
147
+ * Verify a payment with the facilitator.
148
+ * Retries on 5xx and network errors with exponential backoff.
106
149
  */
107
150
  async verifyPayment(paymentSignatureHeader, requirements) {
108
151
  try {
109
152
  const paymentPayload = decodeBase64Json(paymentSignatureHeader);
110
- const verifyPayload = {
111
- x402Version: 2,
112
- paymentPayload,
113
- paymentRequirements: requirements
114
- };
115
- const response = await fetch(`${this.facilitatorUrl}/verify`, {
153
+ const response = await this.fetchWithRetry(`${this.facilitatorUrl}/verify`, {
116
154
  method: "POST",
117
- headers: {
118
- "Content-Type": "application/json"
119
- },
120
- body: JSON.stringify(verifyPayload)
155
+ headers: { "Content-Type": "application/json" },
156
+ body: JSON.stringify({
157
+ x402Version: 2,
158
+ paymentPayload,
159
+ paymentRequirements: requirements
160
+ })
121
161
  });
122
162
  if (!response.ok) {
123
- const errorText = await response.text();
124
- console.error(`Facilitator /verify returned ${response.status}:`, errorText);
125
163
  return {
126
164
  isValid: false,
127
165
  invalidReason: `facilitator_error_${response.status}`
@@ -129,38 +167,27 @@ var FacilitatorClient = class {
129
167
  }
130
168
  return await response.json();
131
169
  } catch (error) {
132
- console.error("Payment verification failed:", error);
133
- return {
134
- isValid: false,
135
- invalidReason: error instanceof Error ? error.message : "unexpected_verify_error"
136
- };
170
+ const reason = error instanceof HttpError ? `facilitator_error_${error.status}` : error instanceof Error && error.name === "AbortError" ? "facilitator_timeout" : error instanceof Error ? error.message : "unexpected_verify_error";
171
+ return { isValid: false, invalidReason: reason };
137
172
  }
138
173
  }
139
174
  /**
140
- * Settle a payment with the facilitator
141
- *
142
- * @param paymentSignatureHeader - Base64-encoded PAYMENT-SIGNATURE header value
143
- * @param requirements - The payment requirements that were sent to the client
144
- * @returns Settlement response with transaction signature on success
175
+ * Settle a payment with the facilitator.
176
+ * Retries on 5xx and network errors with exponential backoff.
145
177
  */
146
178
  async settlePayment(paymentSignatureHeader, requirements) {
147
179
  try {
148
180
  const paymentPayload = decodeBase64Json(paymentSignatureHeader);
149
- const settlePayload = {
150
- x402Version: 2,
151
- paymentPayload,
152
- paymentRequirements: requirements
153
- };
154
- const response = await fetch(`${this.facilitatorUrl}/settle`, {
181
+ const response = await this.fetchWithRetry(`${this.facilitatorUrl}/settle`, {
155
182
  method: "POST",
156
- headers: {
157
- "Content-Type": "application/json"
158
- },
159
- body: JSON.stringify(settlePayload)
183
+ headers: { "Content-Type": "application/json" },
184
+ body: JSON.stringify({
185
+ x402Version: 2,
186
+ paymentPayload,
187
+ paymentRequirements: requirements
188
+ })
160
189
  });
161
190
  if (!response.ok) {
162
- const errorText = await response.text();
163
- console.error(`Facilitator /settle returned ${response.status}:`, errorText);
164
191
  return {
165
192
  success: false,
166
193
  network: requirements.network,
@@ -168,22 +195,27 @@ var FacilitatorClient = class {
168
195
  };
169
196
  }
170
197
  const result = await response.json();
171
- return {
172
- ...result,
173
- network: requirements.network
174
- };
198
+ return { ...result, network: requirements.network };
175
199
  } catch (error) {
176
- console.error("Payment settlement failed:", error);
200
+ const reason = error instanceof HttpError ? `facilitator_error_${error.status}` : error instanceof Error && error.name === "AbortError" ? "facilitator_timeout" : error instanceof Error ? error.message : "unexpected_settle_error";
177
201
  return {
178
202
  success: false,
179
203
  network: requirements.network,
180
- errorReason: error instanceof Error ? error.message : "unexpected_settle_error"
204
+ errorReason: reason
181
205
  };
182
206
  }
183
207
  }
184
208
  };
185
209
 
186
210
  // src/server/x402-server.ts
211
+ function extractAmountFromHeader(paymentSignatureHeader) {
212
+ try {
213
+ const decoded = decodeBase64Json(paymentSignatureHeader);
214
+ return decoded?.accepted?.amount ?? decoded?.accepted?.maxAmountRequired;
215
+ } catch {
216
+ return void 0;
217
+ }
218
+ }
187
219
  function createX402Server(config) {
188
220
  const {
189
221
  payTo,
@@ -194,6 +226,29 @@ function createX402Server(config) {
194
226
  } = config;
195
227
  const facilitator = new FacilitatorClient(facilitatorUrl);
196
228
  let cachedExtra = null;
229
+ const requirementsCache = /* @__PURE__ */ new Map();
230
+ const CACHE_PRUNE_INTERVAL = 3e4;
231
+ let lastPrune = Date.now();
232
+ function cacheRequirements(accept) {
233
+ const ttl = (accept.maxTimeoutSeconds || defaultTimeoutSeconds) * 1e3;
234
+ requirementsCache.set(accept.payTo, { accept, expiresAt: Date.now() + ttl });
235
+ if (Date.now() - lastPrune > CACHE_PRUNE_INTERVAL) {
236
+ const now = Date.now();
237
+ for (const [key, entry] of requirementsCache) {
238
+ if (entry.expiresAt < now) requirementsCache.delete(key);
239
+ }
240
+ lastPrune = now;
241
+ }
242
+ }
243
+ function getCachedRequirements(address) {
244
+ const entry = requirementsCache.get(address);
245
+ if (!entry) return void 0;
246
+ if (entry.expiresAt < Date.now()) {
247
+ requirementsCache.delete(address);
248
+ return void 0;
249
+ }
250
+ return entry.accept;
251
+ }
197
252
  async function resolvePayTo(context) {
198
253
  if (typeof payTo === "string") return payTo;
199
254
  return payTo(context || {});
@@ -219,7 +274,7 @@ function createX402Server(config) {
219
274
  timeoutSeconds = defaultTimeoutSeconds
220
275
  } = options;
221
276
  const extra = await getNetworkExtra();
222
- return {
277
+ const accept = {
223
278
  scheme: "exact",
224
279
  network,
225
280
  amount: amountAtomic,
@@ -229,6 +284,8 @@ function createX402Server(config) {
229
284
  maxTimeoutSeconds: timeoutSeconds,
230
285
  extra
231
286
  };
287
+ cacheRequirements(accept);
288
+ return accept;
232
289
  }
233
290
  async function getPaymentAccept(options) {
234
291
  const address = await resolvePayTo({
@@ -271,14 +328,26 @@ function createX402Server(config) {
271
328
  async function verifyPayment(paymentSignatureHeader, requirements) {
272
329
  if (!requirements) {
273
330
  const address = await resolvePayTo({ paymentHeader: paymentSignatureHeader });
274
- requirements = await buildPaymentAccept(address, { amountAtomic: "0", resourceUrl: "" });
331
+ requirements = getCachedRequirements(address);
332
+ if (!requirements) {
333
+ requirements = await buildPaymentAccept(address, {
334
+ amountAtomic: extractAmountFromHeader(paymentSignatureHeader) ?? "0",
335
+ resourceUrl: ""
336
+ });
337
+ }
275
338
  }
276
339
  return facilitator.verifyPayment(paymentSignatureHeader, requirements);
277
340
  }
278
341
  async function settlePayment(paymentSignatureHeader, requirements) {
279
342
  if (!requirements) {
280
343
  const address = await resolvePayTo({ paymentHeader: paymentSignatureHeader });
281
- requirements = await buildPaymentAccept(address, { amountAtomic: "0", resourceUrl: "" });
344
+ requirements = getCachedRequirements(address);
345
+ if (!requirements) {
346
+ requirements = await buildPaymentAccept(address, {
347
+ amountAtomic: extractAmountFromHeader(paymentSignatureHeader) ?? "0",
348
+ resourceUrl: ""
349
+ });
350
+ }
282
351
  }
283
352
  return facilitator.settlePayment(paymentSignatureHeader, requirements);
284
353
  }
@@ -297,6 +366,15 @@ function createX402Server(config) {
297
366
  // src/server/middleware.ts
298
367
  var DEFAULT_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
299
368
  var USDC_DECIMALS = 6;
369
+ function resolvePayToForNetwork(payTo, network) {
370
+ if (typeof payTo === "string" || typeof payTo === "function") return payTo;
371
+ if (network in payTo) return payTo[network];
372
+ const prefix = network.split(":")[0];
373
+ const globKey = `${prefix}:*`;
374
+ if (globKey in payTo) return payTo[globKey];
375
+ if ("*" in payTo) return payTo["*"];
376
+ throw new Error(`No payTo configured for network "${network}"`);
377
+ }
300
378
  function x402Middleware(config) {
301
379
  const {
302
380
  payTo,
@@ -311,18 +389,29 @@ function x402Middleware(config) {
311
389
  getAmount,
312
390
  getDescription
313
391
  } = config;
314
- const providerDefaults = typeof payTo !== "string" ? payTo._x402Defaults : void 0;
315
- const network = config.network ?? providerDefaults?.network ?? DEFAULT_NETWORK;
316
- const facilitatorUrl = config.facilitatorUrl ?? providerDefaults?.facilitatorUrl;
317
392
  const log = verbose ? console.log.bind(console, "[x402:middleware]") : () => {
318
393
  };
319
- const server = createX402Server({
320
- payTo,
321
- network,
322
- asset,
323
- facilitatorUrl,
324
- defaultTimeoutSeconds: timeoutSeconds
325
- });
394
+ const singleProviderDefaults = typeof payTo === "function" ? payTo._x402Defaults : void 0;
395
+ const facilitatorUrl = config.facilitatorUrl ?? singleProviderDefaults?.facilitatorUrl;
396
+ const configuredNetworks = (() => {
397
+ if (config.network) {
398
+ return Array.isArray(config.network) ? config.network : [config.network];
399
+ }
400
+ if (singleProviderDefaults?.network) return [singleProviderDefaults.network];
401
+ return [DEFAULT_NETWORK];
402
+ })();
403
+ const servers = /* @__PURE__ */ new Map();
404
+ for (const net of configuredNetworks) {
405
+ const netPayTo = resolvePayToForNetwork(payTo, net);
406
+ servers.set(net, createX402Server({
407
+ payTo: netPayTo,
408
+ network: net,
409
+ asset,
410
+ facilitatorUrl,
411
+ defaultTimeoutSeconds: timeoutSeconds
412
+ }));
413
+ }
414
+ const primaryServer = servers.get(configuredNetworks[0]);
326
415
  return async (req, res, next) => {
327
416
  try {
328
417
  const paymentSignature = req.headers["payment-signature"];
@@ -340,8 +429,23 @@ function x402Middleware(config) {
340
429
  mimeType,
341
430
  timeoutSeconds
342
431
  };
343
- const requirements = await server.buildRequirements(requirementsOptions);
344
- const encoded = server.encodeRequirements(requirements);
432
+ const allAccepts = [];
433
+ let requirements = null;
434
+ for (const [, srv] of servers) {
435
+ try {
436
+ const reqs = await srv.buildRequirements(requirementsOptions);
437
+ allAccepts.push(...reqs.accepts);
438
+ if (!requirements) requirements = reqs;
439
+ } catch (e) {
440
+ log("Failed to build requirements for a network:", e);
441
+ }
442
+ }
443
+ if (!requirements || allAccepts.length === 0) {
444
+ res.status(500).json({ error: "Failed to build payment requirements" });
445
+ return;
446
+ }
447
+ requirements = { ...requirements, accepts: allAccepts };
448
+ const encoded = primaryServer.encodeRequirements(requirements);
345
449
  res.setHeader("PAYMENT-REQUIRED", encoded);
346
450
  res.status(402).json({
347
451
  error: "Payment required",
@@ -351,7 +455,16 @@ function x402Middleware(config) {
351
455
  return;
352
456
  }
353
457
  log("Payment signature received, verifying...");
354
- const verifyResult = await server.verifyPayment(paymentSignature);
458
+ let targetServer = primaryServer;
459
+ try {
460
+ const decoded = JSON.parse(Buffer.from(paymentSignature, "base64").toString());
461
+ const paymentNetwork = decoded?.accepted?.network;
462
+ if (paymentNetwork && servers.has(paymentNetwork)) {
463
+ targetServer = servers.get(paymentNetwork);
464
+ }
465
+ } catch {
466
+ }
467
+ const verifyResult = await targetServer.verifyPayment(paymentSignature);
355
468
  if (!verifyResult.isValid) {
356
469
  log("Payment verification failed:", verifyResult.invalidReason);
357
470
  res.status(402).json({
@@ -361,7 +474,7 @@ function x402Middleware(config) {
361
474
  return;
362
475
  }
363
476
  log("Payment verified, settling...");
364
- const settleResult = await server.settlePayment(paymentSignature);
477
+ const settleResult = await targetServer.settlePayment(paymentSignature);
365
478
  if (!settleResult.success) {
366
479
  log("Payment settlement failed:", settleResult.errorReason);
367
480
  res.status(402).json({
@@ -371,15 +484,16 @@ function x402Middleware(config) {
371
484
  return;
372
485
  }
373
486
  log("Payment settled:", settleResult.transaction);
487
+ const settledNetwork = settleResult.network || configuredNetworks[0];
374
488
  req.x402 = {
375
489
  transaction: settleResult.transaction,
376
490
  payer: verifyResult.payer ?? "",
377
- network
491
+ network: settledNetwork
378
492
  };
379
493
  const paymentResponseData = {
380
494
  success: true,
381
495
  transaction: settleResult.transaction,
382
- network,
496
+ network: settledNetwork,
383
497
  payer: verifyResult.payer ?? ""
384
498
  };
385
499
  if (settleResult.extensions) {
@@ -1836,13 +1950,9 @@ function stripePayTo(secretKeyOrConfig) {
1836
1950
  amount: amountInCents,
1837
1951
  currency: "usd",
1838
1952
  payment_method_types: ["crypto"],
1839
- payment_method_data: {
1840
- type: "crypto"
1841
- },
1953
+ payment_method_data: { type: "crypto" },
1842
1954
  payment_method_options: {
1843
- crypto: {
1844
- mode: "custom"
1845
- }
1955
+ crypto: { mode: "custom" }
1846
1956
  },
1847
1957
  confirm: true
1848
1958
  });