@dexterai/x402 1.7.1 → 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.
Files changed (36) hide show
  1. package/README.md +47 -5
  2. package/dist/adapters/index.cjs +408 -412
  3. package/dist/adapters/index.cjs.map +1 -1
  4. package/dist/adapters/index.d.cts +7 -6
  5. package/dist/adapters/index.d.ts +7 -6
  6. package/dist/adapters/index.js +385 -415
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/client/index.cjs +583 -548
  9. package/dist/client/index.cjs.map +1 -1
  10. package/dist/client/index.d.cts +8 -8
  11. package/dist/client/index.d.ts +8 -8
  12. package/dist/client/index.js +594 -555
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/react/index.cjs +413 -398
  15. package/dist/react/index.cjs.map +1 -1
  16. package/dist/react/index.d.cts +4 -4
  17. package/dist/react/index.d.ts +4 -4
  18. package/dist/react/index.js +409 -396
  19. package/dist/react/index.js.map +1 -1
  20. package/dist/server/index.cjs +211 -81
  21. package/dist/server/index.cjs.map +1 -1
  22. package/dist/server/index.d.cts +88 -38
  23. package/dist/server/index.d.ts +88 -38
  24. package/dist/server/index.js +211 -81
  25. package/dist/server/index.js.map +1 -1
  26. package/dist/{solana-CnW6P4lJ.d.cts → solana-BeGAqPta.d.cts} +17 -5
  27. package/dist/{solana-CJdhHls8.d.ts → solana-CQD9yMju.d.ts} +17 -5
  28. package/dist/{types-ClEZ34n4.d.ts → types-B477nBpg.d.cts} +8 -3
  29. package/dist/{types-BB-2vowq.d.cts → types-BWnUAPvD.d.ts} +8 -3
  30. package/dist/{types-C6ty4U6C.d.ts → types-DYLi7SuF.d.cts} +2 -0
  31. package/dist/{types-C6ty4U6C.d.cts → types-DYLi7SuF.d.ts} +2 -0
  32. package/dist/utils/index.cjs.map +1 -1
  33. package/dist/utils/index.js.map +1 -1
  34. package/dist/{x402-client-B9ECWy7k.d.ts → x402-client-D9b3PHai.d.ts} +24 -3
  35. package/dist/{x402-client-BhLOoqwa.d.cts → x402-client-Dk9q2QQF.d.cts} +24 -3
  36. package/package.json +9 -3
@@ -107,25 +107,77 @@ function decodeBase64Json(encoded) {
107
107
  }
108
108
 
109
109
  // src/server/facilitator-client.ts
110
+ function isRetryable(error) {
111
+ if (error instanceof TypeError) return true;
112
+ if (error && typeof error === "object" && "status" in error) {
113
+ const status = error.status;
114
+ return status >= 500 && status < 600;
115
+ }
116
+ return false;
117
+ }
118
+ var HttpError = class extends Error {
119
+ status;
120
+ body;
121
+ constructor(status, body) {
122
+ super(`HTTP ${status}`);
123
+ this.status = status;
124
+ this.body = body;
125
+ }
126
+ };
110
127
  var FacilitatorClient = class {
111
128
  facilitatorUrl;
112
129
  cachedSupported = null;
113
130
  cacheTime = 0;
114
131
  CACHE_TTL_MS = 6e4;
115
- // 1 minute cache
116
- constructor(facilitatorUrl = DEXTER_FACILITATOR_URL) {
132
+ timeoutMs;
133
+ maxRetries;
134
+ retryBaseMs;
135
+ constructor(facilitatorUrl = DEXTER_FACILITATOR_URL, config) {
117
136
  this.facilitatorUrl = facilitatorUrl.replace(/\/$/, "");
137
+ this.timeoutMs = config?.timeoutMs ?? 1e4;
138
+ this.maxRetries = config?.maxRetries ?? 3;
139
+ this.retryBaseMs = config?.retryBaseMs ?? 500;
140
+ }
141
+ async fetchWithTimeout(url, init) {
142
+ const controller = new AbortController();
143
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
144
+ try {
145
+ return await fetch(url, { ...init, signal: controller.signal });
146
+ } finally {
147
+ clearTimeout(timer);
148
+ }
149
+ }
150
+ async fetchWithRetry(url, init) {
151
+ let lastError;
152
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
153
+ try {
154
+ const response = await this.fetchWithTimeout(url, init);
155
+ if (!response.ok && response.status >= 500) {
156
+ throw new HttpError(response.status, await response.text());
157
+ }
158
+ return response;
159
+ } catch (error) {
160
+ lastError = error;
161
+ if (attempt < this.maxRetries - 1 && isRetryable(error)) {
162
+ const delay = this.retryBaseMs * Math.pow(2, attempt);
163
+ await new Promise((r) => setTimeout(r, delay));
164
+ continue;
165
+ }
166
+ throw error;
167
+ }
168
+ }
169
+ throw lastError;
118
170
  }
119
171
  /**
120
- * Get supported payment kinds from the facilitator
121
- * Results are cached for 1 minute to reduce network calls
172
+ * Get supported payment kinds from the facilitator.
173
+ * Results are cached for 1 minute to reduce network calls.
122
174
  */
123
175
  async getSupported() {
124
176
  const now = Date.now();
125
177
  if (this.cachedSupported && now - this.cacheTime < this.CACHE_TTL_MS) {
126
178
  return this.cachedSupported;
127
179
  }
128
- const response = await fetch(`${this.facilitatorUrl}/supported`);
180
+ const response = await this.fetchWithTimeout(`${this.facilitatorUrl}/supported`);
129
181
  if (!response.ok) {
130
182
  throw new Error(`Facilitator /supported returned ${response.status}`);
131
183
  }
@@ -135,9 +187,6 @@ var FacilitatorClient = class {
135
187
  }
136
188
  /**
137
189
  * Get the fee payer address for a specific network
138
- *
139
- * @param network - CAIP-2 network identifier
140
- * @returns Fee payer address
141
190
  */
142
191
  async getFeePayer(network) {
143
192
  const supported = await this.getSupported();
@@ -153,9 +202,6 @@ var FacilitatorClient = class {
153
202
  }
154
203
  /**
155
204
  * Get extra data for a network (feePayer, decimals, EIP-712 data, etc.)
156
- *
157
- * @param network - CAIP-2 network identifier
158
- * @returns Extra data from /supported
159
205
  */
160
206
  async getNetworkExtra(network) {
161
207
  const supported = await this.getSupported();
@@ -165,30 +211,22 @@ var FacilitatorClient = class {
165
211
  return kind?.extra;
166
212
  }
167
213
  /**
168
- * Verify a payment with the facilitator
169
- *
170
- * @param paymentSignatureHeader - Base64-encoded PAYMENT-SIGNATURE header value
171
- * @param requirements - The payment requirements that were sent to the client
172
- * @returns Verification response
214
+ * Verify a payment with the facilitator.
215
+ * Retries on 5xx and network errors with exponential backoff.
173
216
  */
174
217
  async verifyPayment(paymentSignatureHeader, requirements) {
175
218
  try {
176
219
  const paymentPayload = decodeBase64Json(paymentSignatureHeader);
177
- const verifyPayload = {
178
- x402Version: 2,
179
- paymentPayload,
180
- paymentRequirements: requirements
181
- };
182
- const response = await fetch(`${this.facilitatorUrl}/verify`, {
220
+ const response = await this.fetchWithRetry(`${this.facilitatorUrl}/verify`, {
183
221
  method: "POST",
184
- headers: {
185
- "Content-Type": "application/json"
186
- },
187
- body: JSON.stringify(verifyPayload)
222
+ headers: { "Content-Type": "application/json" },
223
+ body: JSON.stringify({
224
+ x402Version: 2,
225
+ paymentPayload,
226
+ paymentRequirements: requirements
227
+ })
188
228
  });
189
229
  if (!response.ok) {
190
- const errorText = await response.text();
191
- console.error(`Facilitator /verify returned ${response.status}:`, errorText);
192
230
  return {
193
231
  isValid: false,
194
232
  invalidReason: `facilitator_error_${response.status}`
@@ -196,38 +234,27 @@ var FacilitatorClient = class {
196
234
  }
197
235
  return await response.json();
198
236
  } catch (error) {
199
- console.error("Payment verification failed:", error);
200
- return {
201
- isValid: false,
202
- invalidReason: error instanceof Error ? error.message : "unexpected_verify_error"
203
- };
237
+ 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";
238
+ return { isValid: false, invalidReason: reason };
204
239
  }
205
240
  }
206
241
  /**
207
- * Settle a payment with the facilitator
208
- *
209
- * @param paymentSignatureHeader - Base64-encoded PAYMENT-SIGNATURE header value
210
- * @param requirements - The payment requirements that were sent to the client
211
- * @returns Settlement response with transaction signature on success
242
+ * Settle a payment with the facilitator.
243
+ * Retries on 5xx and network errors with exponential backoff.
212
244
  */
213
245
  async settlePayment(paymentSignatureHeader, requirements) {
214
246
  try {
215
247
  const paymentPayload = decodeBase64Json(paymentSignatureHeader);
216
- const settlePayload = {
217
- x402Version: 2,
218
- paymentPayload,
219
- paymentRequirements: requirements
220
- };
221
- const response = await fetch(`${this.facilitatorUrl}/settle`, {
248
+ const response = await this.fetchWithRetry(`${this.facilitatorUrl}/settle`, {
222
249
  method: "POST",
223
- headers: {
224
- "Content-Type": "application/json"
225
- },
226
- body: JSON.stringify(settlePayload)
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify({
252
+ x402Version: 2,
253
+ paymentPayload,
254
+ paymentRequirements: requirements
255
+ })
227
256
  });
228
257
  if (!response.ok) {
229
- const errorText = await response.text();
230
- console.error(`Facilitator /settle returned ${response.status}:`, errorText);
231
258
  return {
232
259
  success: false,
233
260
  network: requirements.network,
@@ -235,22 +262,27 @@ var FacilitatorClient = class {
235
262
  };
236
263
  }
237
264
  const result = await response.json();
238
- return {
239
- ...result,
240
- network: requirements.network
241
- };
265
+ return { ...result, network: requirements.network };
242
266
  } catch (error) {
243
- console.error("Payment settlement failed:", error);
267
+ 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";
244
268
  return {
245
269
  success: false,
246
270
  network: requirements.network,
247
- errorReason: error instanceof Error ? error.message : "unexpected_settle_error"
271
+ errorReason: reason
248
272
  };
249
273
  }
250
274
  }
251
275
  };
252
276
 
253
277
  // src/server/x402-server.ts
278
+ function extractAmountFromHeader(paymentSignatureHeader) {
279
+ try {
280
+ const decoded = decodeBase64Json(paymentSignatureHeader);
281
+ return decoded?.accepted?.amount ?? decoded?.accepted?.maxAmountRequired;
282
+ } catch {
283
+ return void 0;
284
+ }
285
+ }
254
286
  function createX402Server(config) {
255
287
  const {
256
288
  payTo,
@@ -261,6 +293,29 @@ function createX402Server(config) {
261
293
  } = config;
262
294
  const facilitator = new FacilitatorClient(facilitatorUrl);
263
295
  let cachedExtra = null;
296
+ const requirementsCache = /* @__PURE__ */ new Map();
297
+ const CACHE_PRUNE_INTERVAL = 3e4;
298
+ let lastPrune = Date.now();
299
+ function cacheRequirements(accept) {
300
+ const ttl = (accept.maxTimeoutSeconds || defaultTimeoutSeconds) * 1e3;
301
+ requirementsCache.set(accept.payTo, { accept, expiresAt: Date.now() + ttl });
302
+ if (Date.now() - lastPrune > CACHE_PRUNE_INTERVAL) {
303
+ const now = Date.now();
304
+ for (const [key, entry] of requirementsCache) {
305
+ if (entry.expiresAt < now) requirementsCache.delete(key);
306
+ }
307
+ lastPrune = now;
308
+ }
309
+ }
310
+ function getCachedRequirements(address) {
311
+ const entry = requirementsCache.get(address);
312
+ if (!entry) return void 0;
313
+ if (entry.expiresAt < Date.now()) {
314
+ requirementsCache.delete(address);
315
+ return void 0;
316
+ }
317
+ return entry.accept;
318
+ }
264
319
  async function resolvePayTo(context) {
265
320
  if (typeof payTo === "string") return payTo;
266
321
  return payTo(context || {});
@@ -286,7 +341,7 @@ function createX402Server(config) {
286
341
  timeoutSeconds = defaultTimeoutSeconds
287
342
  } = options;
288
343
  const extra = await getNetworkExtra();
289
- return {
344
+ const accept = {
290
345
  scheme: "exact",
291
346
  network,
292
347
  amount: amountAtomic,
@@ -296,6 +351,8 @@ function createX402Server(config) {
296
351
  maxTimeoutSeconds: timeoutSeconds,
297
352
  extra
298
353
  };
354
+ cacheRequirements(accept);
355
+ return accept;
299
356
  }
300
357
  async function getPaymentAccept(options) {
301
358
  const address = await resolvePayTo({
@@ -338,14 +395,26 @@ function createX402Server(config) {
338
395
  async function verifyPayment(paymentSignatureHeader, requirements) {
339
396
  if (!requirements) {
340
397
  const address = await resolvePayTo({ paymentHeader: paymentSignatureHeader });
341
- requirements = await buildPaymentAccept(address, { amountAtomic: "0", resourceUrl: "" });
398
+ requirements = getCachedRequirements(address);
399
+ if (!requirements) {
400
+ requirements = await buildPaymentAccept(address, {
401
+ amountAtomic: extractAmountFromHeader(paymentSignatureHeader) ?? "0",
402
+ resourceUrl: ""
403
+ });
404
+ }
342
405
  }
343
406
  return facilitator.verifyPayment(paymentSignatureHeader, requirements);
344
407
  }
345
408
  async function settlePayment(paymentSignatureHeader, requirements) {
346
409
  if (!requirements) {
347
410
  const address = await resolvePayTo({ paymentHeader: paymentSignatureHeader });
348
- requirements = await buildPaymentAccept(address, { amountAtomic: "0", resourceUrl: "" });
411
+ requirements = getCachedRequirements(address);
412
+ if (!requirements) {
413
+ requirements = await buildPaymentAccept(address, {
414
+ amountAtomic: extractAmountFromHeader(paymentSignatureHeader) ?? "0",
415
+ resourceUrl: ""
416
+ });
417
+ }
349
418
  }
350
419
  return facilitator.settlePayment(paymentSignatureHeader, requirements);
351
420
  }
@@ -364,6 +433,15 @@ function createX402Server(config) {
364
433
  // src/server/middleware.ts
365
434
  var DEFAULT_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
366
435
  var USDC_DECIMALS = 6;
436
+ function resolvePayToForNetwork(payTo, network) {
437
+ if (typeof payTo === "string" || typeof payTo === "function") return payTo;
438
+ if (network in payTo) return payTo[network];
439
+ const prefix = network.split(":")[0];
440
+ const globKey = `${prefix}:*`;
441
+ if (globKey in payTo) return payTo[globKey];
442
+ if ("*" in payTo) return payTo["*"];
443
+ throw new Error(`No payTo configured for network "${network}"`);
444
+ }
367
445
  function x402Middleware(config) {
368
446
  const {
369
447
  payTo,
@@ -378,18 +456,29 @@ function x402Middleware(config) {
378
456
  getAmount,
379
457
  getDescription
380
458
  } = config;
381
- const providerDefaults = typeof payTo !== "string" ? payTo._x402Defaults : void 0;
382
- const network = config.network ?? providerDefaults?.network ?? DEFAULT_NETWORK;
383
- const facilitatorUrl = config.facilitatorUrl ?? providerDefaults?.facilitatorUrl;
384
459
  const log = verbose ? console.log.bind(console, "[x402:middleware]") : () => {
385
460
  };
386
- const server = createX402Server({
387
- payTo,
388
- network,
389
- asset,
390
- facilitatorUrl,
391
- defaultTimeoutSeconds: timeoutSeconds
392
- });
461
+ const singleProviderDefaults = typeof payTo === "function" ? payTo._x402Defaults : void 0;
462
+ const facilitatorUrl = config.facilitatorUrl ?? singleProviderDefaults?.facilitatorUrl;
463
+ const configuredNetworks = (() => {
464
+ if (config.network) {
465
+ return Array.isArray(config.network) ? config.network : [config.network];
466
+ }
467
+ if (singleProviderDefaults?.network) return [singleProviderDefaults.network];
468
+ return [DEFAULT_NETWORK];
469
+ })();
470
+ const servers = /* @__PURE__ */ new Map();
471
+ for (const net of configuredNetworks) {
472
+ const netPayTo = resolvePayToForNetwork(payTo, net);
473
+ servers.set(net, createX402Server({
474
+ payTo: netPayTo,
475
+ network: net,
476
+ asset,
477
+ facilitatorUrl,
478
+ defaultTimeoutSeconds: timeoutSeconds
479
+ }));
480
+ }
481
+ const primaryServer = servers.get(configuredNetworks[0]);
393
482
  return async (req, res, next) => {
394
483
  try {
395
484
  const paymentSignature = req.headers["payment-signature"];
@@ -407,8 +496,23 @@ function x402Middleware(config) {
407
496
  mimeType,
408
497
  timeoutSeconds
409
498
  };
410
- const requirements = await server.buildRequirements(requirementsOptions);
411
- const encoded = server.encodeRequirements(requirements);
499
+ const allAccepts = [];
500
+ let requirements = null;
501
+ for (const [, srv] of servers) {
502
+ try {
503
+ const reqs = await srv.buildRequirements(requirementsOptions);
504
+ allAccepts.push(...reqs.accepts);
505
+ if (!requirements) requirements = reqs;
506
+ } catch (e) {
507
+ log("Failed to build requirements for a network:", e);
508
+ }
509
+ }
510
+ if (!requirements || allAccepts.length === 0) {
511
+ res.status(500).json({ error: "Failed to build payment requirements" });
512
+ return;
513
+ }
514
+ requirements = { ...requirements, accepts: allAccepts };
515
+ const encoded = primaryServer.encodeRequirements(requirements);
412
516
  res.setHeader("PAYMENT-REQUIRED", encoded);
413
517
  res.status(402).json({
414
518
  error: "Payment required",
@@ -418,7 +522,16 @@ function x402Middleware(config) {
418
522
  return;
419
523
  }
420
524
  log("Payment signature received, verifying...");
421
- const verifyResult = await server.verifyPayment(paymentSignature);
525
+ let targetServer = primaryServer;
526
+ try {
527
+ const decoded = JSON.parse(Buffer.from(paymentSignature, "base64").toString());
528
+ const paymentNetwork = decoded?.accepted?.network;
529
+ if (paymentNetwork && servers.has(paymentNetwork)) {
530
+ targetServer = servers.get(paymentNetwork);
531
+ }
532
+ } catch {
533
+ }
534
+ const verifyResult = await targetServer.verifyPayment(paymentSignature);
422
535
  if (!verifyResult.isValid) {
423
536
  log("Payment verification failed:", verifyResult.invalidReason);
424
537
  res.status(402).json({
@@ -428,7 +541,7 @@ function x402Middleware(config) {
428
541
  return;
429
542
  }
430
543
  log("Payment verified, settling...");
431
- const settleResult = await server.settlePayment(paymentSignature);
544
+ const settleResult = await targetServer.settlePayment(paymentSignature);
432
545
  if (!settleResult.success) {
433
546
  log("Payment settlement failed:", settleResult.errorReason);
434
547
  res.status(402).json({
@@ -438,18 +551,39 @@ function x402Middleware(config) {
438
551
  return;
439
552
  }
440
553
  log("Payment settled:", settleResult.transaction);
554
+ const settledNetwork = settleResult.network || configuredNetworks[0];
441
555
  req.x402 = {
442
556
  transaction: settleResult.transaction,
443
557
  payer: verifyResult.payer ?? "",
444
- network
558
+ network: settledNetwork
445
559
  };
446
560
  const paymentResponseData = {
447
561
  success: true,
448
562
  transaction: settleResult.transaction,
449
- network,
563
+ network: settledNetwork,
450
564
  payer: verifyResult.payer ?? ""
451
565
  };
566
+ if (settleResult.extensions) {
567
+ paymentResponseData.extensions = settleResult.extensions;
568
+ }
452
569
  res.setHeader("PAYMENT-RESPONSE", encodeBase64Json(paymentResponseData));
570
+ if (config.sponsoredAccess && settleResult.extensions?.["sponsored-access"]) {
571
+ const extData = settleResult.extensions["sponsored-access"];
572
+ const recs = extData?.info?.recommendations;
573
+ if (recs && recs.length > 0) {
574
+ log("Injecting sponsored-access recommendations into response");
575
+ const originalJson = res.json.bind(res);
576
+ res.json = function patchedJson(body) {
577
+ if (typeof config.sponsoredAccess === "object" && config.sponsoredAccess.inject) {
578
+ return originalJson(config.sponsoredAccess.inject(body, recs));
579
+ }
580
+ if (body && typeof body === "object" && !Array.isArray(body)) {
581
+ return originalJson({ _x402_sponsored: recs, ...body });
582
+ }
583
+ return originalJson(body);
584
+ };
585
+ }
586
+ }
453
587
  next();
454
588
  } catch (error) {
455
589
  log("Middleware error:", error);
@@ -1883,13 +2017,9 @@ function stripePayTo(secretKeyOrConfig) {
1883
2017
  amount: amountInCents,
1884
2018
  currency: "usd",
1885
2019
  payment_method_types: ["crypto"],
1886
- payment_method_data: {
1887
- type: "crypto"
1888
- },
2020
+ payment_method_data: { type: "crypto" },
1889
2021
  payment_method_options: {
1890
- crypto: {
1891
- mode: "custom"
1892
- }
2022
+ crypto: { mode: "custom" }
1893
2023
  },
1894
2024
  confirm: true
1895
2025
  });