@parallel-protocol/x402 0.5.3 → 0.7.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.
@@ -1,27 +1,51 @@
1
- import { z } from 'zod';
1
+ import { X402_ERROR_CODES, X402RuntimeError, X402ConfigError, isPriceRejection } from './chunk-AFRDHGDT.js';
2
2
  import { getTokenDecimals as getTokenDecimals$1 } from '@parallel-protocol/chains';
3
+ import { z } from 'zod';
3
4
 
4
- // src/errors.ts
5
- var X402_ERROR_CODES = {
6
- FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
7
- FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
8
- INVALID_PAYMENT: "INVALID_PAYMENT"
9
- };
10
- var X402ConfigError = class extends Error {
11
- constructor(message, cause) {
12
- super(message);
13
- this.name = "X402ConfigError";
14
- this.cause = cause;
15
- }
16
- };
17
- var X402RuntimeError = class extends Error {
18
- constructor(code) {
19
- super(code);
20
- this.code = code;
21
- this.name = "X402RuntimeError";
5
+ var PARALLEL_TOKENS = {
6
+ ethereum: {
7
+ usdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
8
+ susdp: "0xd3a452B305C8285c0Dd7b8537665c734d3D279eF",
9
+ usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
10
+ frxUSD: "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29",
11
+ sfrxUSD: "0xcf62F905562626CfcDD2261162a51fd02Fc9c5b6",
12
+ USDe: "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3",
13
+ sUSDe: "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497"
14
+ },
15
+ base: {
16
+ usdp: "0x76A9A0062ec6712b99B4f63bD2b4270185759dd5",
17
+ susdp: "0x472eD57b376fE400259FB28e5C46eB53f0E3e7E7",
18
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
19
+ USDS: "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc",
20
+ sUSDS: "0x5875eEE11Cf8398102FdAd704C9E96607675467a"
21
+ },
22
+ avalanche: {
23
+ usdp: "0x9eE1963f05553eF838604Dd39403be21ceF26AA4",
24
+ susdp: "0x9d92c21205383651610f90722131655a5b8ed3e0",
25
+ usdc: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
26
+ ygamiUSDC: "0x9fD32FD5e32C6B95483d36C5E724C5C5250Ce010"
27
+ },
28
+ hyperevm: {
29
+ usdp: "0xBE65F0F410A72BeC163dC65d46c83699e957D588",
30
+ susdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
31
+ usdc: "0xb88339cb7199b77e23db6e890353e22632ba630f",
32
+ USDe: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34",
33
+ sUSDe: "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2"
22
34
  }
23
- code;
24
35
  };
36
+ function getDefaultAcceptedTokens(network) {
37
+ const entry = PARALLEL_TOKENS[network];
38
+ if (!entry) return void 0;
39
+ return [entry.usdp, entry.usdc, entry.susdp];
40
+ }
41
+ function getMeteredAcceptedTokens(network) {
42
+ const entry = PARALLEL_TOKENS[network];
43
+ if (!entry) return void 0;
44
+ return [entry.usdp];
45
+ }
46
+ function getTokenDecimals(network, address) {
47
+ return getTokenDecimals$1(network, address);
48
+ }
25
49
 
26
50
  // src/utils.ts
27
51
  var EIP155_NETWORKS = {
@@ -45,6 +69,28 @@ function parsePrice(price, decimals = 18) {
45
69
  `Invalid price format: "${price}". Expected "0.01" or a bigint.`
46
70
  );
47
71
  }
72
+ function isJsonContentType(contentType) {
73
+ return /^application\/(?:[\w.-]+\+)?json(?:\s*;.*)?$/i.test(
74
+ contentType?.trim() ?? ""
75
+ );
76
+ }
77
+ function parseJsonBody(contentType, text) {
78
+ if (text === void 0 || !isJsonContentType(contentType)) return void 0;
79
+ try {
80
+ return JSON.parse(text);
81
+ } catch {
82
+ return void 0;
83
+ }
84
+ }
85
+ async function jsonFromResponse(res) {
86
+ const contentType = res.headers.get("content-type") ?? void 0;
87
+ if (!isJsonContentType(contentType)) return void 0;
88
+ try {
89
+ return parseJsonBody(contentType, await res.clone().text());
90
+ } catch {
91
+ return void 0;
92
+ }
93
+ }
48
94
  function validateAddress(address) {
49
95
  return /^0x[0-9a-fA-F]{40}$/.test(address);
50
96
  }
@@ -74,19 +120,44 @@ var ethereumAddressSchema = z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid Eth
74
120
  var txHashSchema = z.string().regex(/^0x[0-9a-fA-F]{64}$/);
75
121
  var priceSchema = z.union([
76
122
  z.bigint().positive("Price must be greater than 0"),
77
- z.string().regex(/^\d+(?:\.\d+)?$/, 'Price must be in format "0.01"').refine((price) => Number(price) > 0, "Price must be greater than 0")
123
+ z.string().regex(/^\d+(?:\.\d+)?$/, 'Price must be in format "0.01"').refine((price) => Number(price) > 0, "Price must be greater than 0"),
124
+ // A per-request price function; its return value is checked per request.
125
+ z.custom(
126
+ (value) => typeof value === "function",
127
+ "Price must be a string, a bigint or a function of the request"
128
+ )
78
129
  ]);
79
130
  var facilitatorConfigSchema = z.object({
80
131
  url: z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
81
132
  apiKey: z.string().optional()
82
133
  });
134
+ var UPTO_MAX_TIMEOUT_SECONDS = 3600;
83
135
  var routeConfigSchema = z.object({
84
136
  price: priceSchema,
85
137
  network: z.string().min(1, "Network is required"),
86
138
  payTo: ethereumAddressSchema,
87
139
  acceptedTokens: z.array(ethereumAddressSchema).optional(),
88
- description: z.string().optional()
89
- });
140
+ description: z.string().optional(),
141
+ scheme: z.enum(["exact", "upto"]).optional(),
142
+ maxTimeoutSeconds: z.number().int().positive().optional(),
143
+ meter: z.custom(
144
+ (value) => typeof value === "function",
145
+ "meter must be a function of the response"
146
+ ).optional()
147
+ }).refine(
148
+ (route) => route.meter === void 0 || route.scheme === "upto",
149
+ "meter only applies to upto routes"
150
+ ).refine(
151
+ (route) => route.scheme !== "upto" || (route.maxTimeoutSeconds ?? 300) <= UPTO_MAX_TIMEOUT_SECONDS,
152
+ `maxTimeoutSeconds must be at most ${UPTO_MAX_TIMEOUT_SECONDS} on an upto route`
153
+ ).refine((route) => {
154
+ if (route.scheme !== "upto" || !route.acceptedTokens?.length) return true;
155
+ const usdp = PARALLEL_TOKENS[route.network]?.usdp;
156
+ if (!usdp) return true;
157
+ return route.acceptedTokens.every(
158
+ (token) => token.toLowerCase() === usdp.toLowerCase()
159
+ );
160
+ }, "acceptedTokens on an upto route may only list the network's USDp");
90
161
  var paymentMiddlewareConfigSchema = z.object({
91
162
  facilitator: facilitatorConfigSchema,
92
163
  routes: z.record(z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
@@ -98,7 +169,8 @@ var facilitatorResponseSchema = z.discriminatedUnion("success", [
98
169
  route: z.string(),
99
170
  chain: z.string(),
100
171
  gasSponsored: z.boolean(),
101
- networkId: z.string()
172
+ networkId: z.string(),
173
+ amountSettled: z.string().optional()
102
174
  }),
103
175
  z.object({
104
176
  success: z.literal(false),
@@ -115,7 +187,9 @@ var verifyResponseSchema = z.object({
115
187
  var settleSuccessSchema = z.object({
116
188
  txHash: txHashSchema,
117
189
  route: z.string(),
118
- gasSponsored: z.boolean()
190
+ gasSponsored: z.boolean(),
191
+ // Metered routes: the amount actually charged (smallest unit).
192
+ amountSettled: z.string().optional()
119
193
  });
120
194
  var settleErrorSchema = z.object({
121
195
  error: z.string(),
@@ -167,14 +241,21 @@ var FacilitatorClient = class {
167
241
  throw new X402RuntimeError(X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
168
242
  }
169
243
  }
170
- async settle(paymentHeader, paymentRequirements) {
244
+ async settle(paymentHeader, paymentRequirements, options) {
171
245
  const paymentPayload = decodePaymentHeader(paymentHeader);
172
246
  let json;
173
247
  try {
174
248
  const response = await fetch(`${this.config.url}/x402/settle`, {
175
249
  method: "POST",
176
250
  headers: this.buildHeaders(),
177
- body: JSON.stringify({ paymentPayload, paymentRequirements }),
251
+ // settleAmount only travels for metered routes — exact bodies are unchanged.
252
+ body: JSON.stringify({
253
+ paymentPayload,
254
+ paymentRequirements,
255
+ ...options?.amount !== void 0 && {
256
+ settleAmount: options.amount
257
+ }
258
+ }),
178
259
  signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
179
260
  });
180
261
  if (!response.ok) {
@@ -197,7 +278,10 @@ var FacilitatorClient = class {
197
278
  route: parsed.route,
198
279
  chain: paymentRequirements.network,
199
280
  gasSponsored: parsed.gasSponsored,
200
- networkId: toEip155Network(paymentRequirements.network)
281
+ networkId: toEip155Network(paymentRequirements.network),
282
+ ...parsed.amountSettled !== void 0 && {
283
+ amountSettled: parsed.amountSettled
284
+ }
201
285
  };
202
286
  }
203
287
  return {
@@ -242,52 +326,146 @@ var FacilitatorClient = class {
242
326
  return headers;
243
327
  }
244
328
  };
245
- var PARALLEL_TOKENS = {
246
- ethereum: {
247
- usdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
248
- susdp: "0xd3a452B305C8285c0Dd7b8537665c734d3D279eF",
249
- usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
250
- frxUSD: "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29",
251
- sfrxUSD: "0xcf62F905562626CfcDD2261162a51fd02Fc9c5b6",
252
- USDe: "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3",
253
- sUSDe: "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497"
254
- },
255
- base: {
256
- usdp: "0x76A9A0062ec6712b99B4f63bD2b4270185759dd5",
257
- susdp: "0x472eD57b376fE400259FB28e5C46eB53f0E3e7E7",
258
- usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
259
- USDS: "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc",
260
- sUSDS: "0x5875eEE11Cf8398102FdAd704C9E96607675467a"
261
- },
262
- avalanche: {
263
- usdp: "0x9eE1963f05553eF838604Dd39403be21ceF26AA4",
264
- susdp: "0x9d92c21205383651610f90722131655a5b8ed3e0",
265
- usdc: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
266
- ygamiUSDC: "0x9fD32FD5e32C6B95483d36C5E724C5C5250Ce010"
267
- },
268
- hyperevm: {
269
- usdp: "0xBE65F0F410A72BeC163dC65d46c83699e957D588",
270
- susdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
271
- usdc: "0xb88339cb7199b77e23db6e890353e22632ba630f",
272
- USDe: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34",
273
- sUSDe: "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2"
329
+
330
+ // src/timeout.ts
331
+ var HANDLER_TIMEOUT_MS = 3e4;
332
+ var HANDLER_TIMEOUT = /* @__PURE__ */ Symbol("x402:handler-timeout");
333
+ async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
334
+ let timer;
335
+ const timeout = new Promise((resolve) => {
336
+ timer = setTimeout(() => resolve(HANDLER_TIMEOUT), ms);
337
+ timer.unref?.();
338
+ });
339
+ try {
340
+ return await Promise.race([promise, timeout]);
341
+ } finally {
342
+ clearTimeout(timer);
274
343
  }
275
- };
276
- function getDefaultAcceptedTokens(network) {
277
- const entry = PARALLEL_TOKENS[network];
278
- if (!entry) return void 0;
279
- return [entry.usdp, entry.usdc, entry.susdp];
280
- }
281
- function getTokenDecimals(network, address) {
282
- return getTokenDecimals$1(network, address);
283
344
  }
284
345
 
285
346
  // src/middleware.ts
347
+ var PRICE_FORMAT = /^\d+(?:\.\d+)?$/;
348
+ async function requestContext(adapter) {
349
+ return {
350
+ method: adapter.getMethod(),
351
+ path: adapter.getPath(),
352
+ url: adapter.getUrl(),
353
+ header: (name) => adapter.getHeader(name),
354
+ body: adapter.getBody ? await adapter.getBody() : void 0
355
+ };
356
+ }
357
+ async function resolvePrice(config, ctx) {
358
+ if (typeof config.price !== "function") return config.price;
359
+ if (!ctx) throw new X402ConfigError("A price function needs the request");
360
+ let price;
361
+ try {
362
+ price = await config.price(ctx);
363
+ } catch (cause) {
364
+ if (isPriceRejection(cause)) throw cause;
365
+ throw new X402ConfigError(
366
+ `The price function of route "${config.network}${ctx.path}" threw`,
367
+ cause
368
+ );
369
+ }
370
+ const valid = typeof price === "bigint" ? price > 0n : typeof price === "string" && PRICE_FORMAT.test(price) && Number(price) > 0;
371
+ if (!valid) {
372
+ throw new X402ConfigError(
373
+ `The price function of route "${config.network}${ctx.path}" returned ${JSON.stringify(
374
+ typeof price === "bigint" ? price.toString() : price
375
+ )} \u2014 expected a positive amount like "0.01" or a bigint`
376
+ );
377
+ }
378
+ return price;
379
+ }
380
+ var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
381
+ var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
382
+ function handlerTimeoutFor(config) {
383
+ if ((config.scheme ?? "exact") !== "upto") return HANDLER_TIMEOUT_MS;
384
+ const seconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
385
+ return Math.max(HANDLER_TIMEOUT_MS, (seconds - 60) * 1e3);
386
+ }
387
+ function resolveSettleAmount(raw, maxAmount, decimals) {
388
+ if (raw === void 0)
389
+ return { kind: "settle", amount: maxAmount.toString() };
390
+ const value = raw.trim();
391
+ let amount;
392
+ try {
393
+ amount = parsePrice(value, decimals);
394
+ } catch {
395
+ return { kind: "invalid", reason: "malformed" };
396
+ }
397
+ const fraction = value.split(".")[1] ?? "";
398
+ if (fraction.length > decimals)
399
+ return { kind: "invalid", reason: "precision" };
400
+ if (amount === 0n) return { kind: "skip" };
401
+ if (amount > maxAmount) return { kind: "invalid", reason: "above-cap" };
402
+ return { kind: "settle", amount: amount.toString() };
403
+ }
404
+ var INVALID_SETTLE_AMOUNT_RESPONSE = {
405
+ error: "INVALID_SETTLE_AMOUNT",
406
+ message: `The handler set an invalid ${SETTLE_AMOUNT_HEADER} header \u2014 expected a decimal amount in token units, formatted like the route price and within the token's precision, at most the route price.`
407
+ };
408
+ var INVALID_METER_RESPONSE = {
409
+ error: "INVALID_SETTLE_AMOUNT",
410
+ message: "The route's meter function failed or returned something that is not an amount; the response was discarded and nothing was charged."
411
+ };
412
+ function invalidSettleAmountResponse(reason) {
413
+ return reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE;
414
+ }
415
+ var AMOUNT_FORMAT = /^\d+(?:\.\d+)?$/;
416
+ async function resolveCharge(verified, settleHeader, response) {
417
+ if (verified.meter) {
418
+ if (!verified.request) {
419
+ throw new X402ConfigError(
420
+ "A metered payment carries a meter but no request context"
421
+ );
422
+ }
423
+ let result;
424
+ try {
425
+ const body = await raceTimeout(
426
+ response.body(),
427
+ verified.handlerTimeoutMs
428
+ );
429
+ if (body === HANDLER_TIMEOUT) return { kind: "invalid", reason: "meter" };
430
+ result = await verified.meter({
431
+ status: response.status,
432
+ header: response.header,
433
+ body,
434
+ request: verified.request
435
+ });
436
+ } catch {
437
+ return { kind: "invalid", reason: "meter" };
438
+ }
439
+ if (result != null) {
440
+ let amount;
441
+ if (typeof result === "bigint") {
442
+ if (result < 0n) return { kind: "invalid", reason: "meter" };
443
+ amount = result;
444
+ } else if (typeof result === "string" && AMOUNT_FORMAT.test(result)) {
445
+ const fraction = result.split(".")[1] ?? "";
446
+ if (fraction.length > verified.decimals) {
447
+ return { kind: "invalid", reason: "meter" };
448
+ }
449
+ amount = parsePrice(result, verified.decimals);
450
+ } else {
451
+ return { kind: "invalid", reason: "meter" };
452
+ }
453
+ if (amount === 0n) return { kind: "skip" };
454
+ if (amount > verified.maxAmount) amount = verified.maxAmount;
455
+ return { kind: "settle", amount: amount.toString() };
456
+ }
457
+ }
458
+ return resolveSettleAmount(
459
+ settleHeader,
460
+ verified.maxAmount,
461
+ verified.decimals
462
+ );
463
+ }
286
464
  function resolveAcceptedTokens(config) {
287
465
  if (config.acceptedTokens?.length) {
288
466
  return config.acceptedTokens;
289
467
  }
290
- const defaults = getDefaultAcceptedTokens(config.network);
468
+ const defaults = config.scheme === "upto" ? getMeteredAcceptedTokens(config.network) : getDefaultAcceptedTokens(config.network);
291
469
  if (defaults) return defaults;
292
470
  throw new X402ConfigError(
293
471
  `acceptedTokens is required for network "${config.network}" \u2014 no defaults available for this chain`
@@ -302,17 +480,26 @@ function resolveDecimals(config, asset) {
302
480
  }
303
481
  return decimals;
304
482
  }
305
- function buildPaymentRequired(url, config, resolvedTokens) {
483
+ function priceInUnits(price, decimals, asset) {
484
+ const units = parsePrice(price, decimals);
485
+ if (units === 0n) {
486
+ throw new X402ConfigError(
487
+ `Price ${typeof price === "bigint" ? price.toString() : `"${price}"`} is below the precision of token ${asset} (${decimals} decimals)`
488
+ );
489
+ }
490
+ return units.toString();
491
+ }
492
+ function buildPaymentRequired(url, config, resolvedTokens, price) {
306
493
  const network = toEip155Network(config.network);
307
494
  const accepts = resolvedTokens.map((asset) => {
308
495
  const decimals = resolveDecimals(config, asset);
309
496
  return {
310
- scheme: "exact",
497
+ scheme: config.scheme ?? "exact",
311
498
  network,
312
499
  asset,
313
- amount: parsePrice(config.price, decimals).toString(),
500
+ amount: priceInUnits(price, decimals, asset),
314
501
  payTo: config.payTo,
315
- maxTimeoutSeconds: 300,
502
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
316
503
  extra: { decimals }
317
504
  };
318
505
  });
@@ -356,17 +543,28 @@ function resolveDeliveredAsset(header, config, resolvedTokens) {
356
543
  const lower = target?.toLowerCase();
357
544
  return resolvedTokens.find((token) => token.toLowerCase() === lower) ?? resolvedTokens[0];
358
545
  }
359
- function buildFacilitatorRequirements(config, asset) {
546
+ function buildFacilitatorRequirements(config, asset, price) {
360
547
  const decimals = resolveDecimals(config, asset);
548
+ const scheme = config.scheme ?? "exact";
361
549
  return {
362
- maxAmountRequired: parsePrice(config.price, decimals).toString(),
550
+ maxAmountRequired: priceInUnits(price, decimals, asset),
363
551
  asset,
364
552
  payTo: config.payTo,
365
- network: config.network
553
+ network: config.network,
554
+ // Only metered routes name their scheme — exact requirements are unchanged.
555
+ ...scheme === "upto" && {
556
+ scheme,
557
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS
558
+ }
366
559
  };
367
560
  }
368
- function paymentRequiredResponse(url, config, resolvedTokens, error) {
369
- const paymentRequired = buildPaymentRequired(url, config, resolvedTokens);
561
+ function paymentRequiredResponse(url, config, resolvedTokens, price, error) {
562
+ const paymentRequired = buildPaymentRequired(
563
+ url,
564
+ config,
565
+ resolvedTokens,
566
+ price
567
+ );
370
568
  if (error) paymentRequired.error = error;
371
569
  return {
372
570
  status: 402,
@@ -394,6 +592,22 @@ function createPaymentGate(config) {
394
592
  const routeConfig = matchRoute(adapter.getPath(), config.routes);
395
593
  if (!routeConfig) return { type: "pass" };
396
594
  const resolvedTokens = resolveAcceptedTokens(routeConfig);
595
+ let request = typeof routeConfig.price === "function" ? await requestContext(adapter) : void 0;
596
+ let price;
597
+ try {
598
+ price = await resolvePrice(routeConfig, request);
599
+ } catch (err) {
600
+ if (!isPriceRejection(err)) throw err;
601
+ const status = Number.isInteger(err.status) && err.status >= 400 && err.status <= 599 ? err.status : 400;
602
+ return {
603
+ type: "error",
604
+ result: {
605
+ status,
606
+ headers: { "Content-Type": "application/json" },
607
+ body: { error: err.code, message: err.message }
608
+ }
609
+ };
610
+ }
397
611
  const paymentHeader = adapter.getHeader("payment-signature") ?? adapter.getHeader("x-payment");
398
612
  if (!paymentHeader) {
399
613
  return {
@@ -401,13 +615,20 @@ function createPaymentGate(config) {
401
615
  result: paymentRequiredResponse(
402
616
  adapter.getUrl(),
403
617
  routeConfig,
404
- resolvedTokens
618
+ resolvedTokens,
619
+ price
405
620
  )
406
621
  };
407
622
  }
623
+ const asset = resolveDeliveredAsset(
624
+ paymentHeader,
625
+ routeConfig,
626
+ resolvedTokens
627
+ );
408
628
  const requirements = buildFacilitatorRequirements(
409
629
  routeConfig,
410
- resolveDeliveredAsset(paymentHeader, routeConfig, resolvedTokens)
630
+ asset,
631
+ price
411
632
  );
412
633
  try {
413
634
  await client.verify(paymentHeader, requirements);
@@ -419,13 +640,21 @@ function createPaymentGate(config) {
419
640
  adapter.getUrl(),
420
641
  routeConfig,
421
642
  resolvedTokens,
643
+ price,
422
644
  code
423
645
  )
424
646
  };
425
647
  }
648
+ if (routeConfig.meter && !request) request = await requestContext(adapter);
426
649
  return {
427
650
  type: "verified",
428
- settle: () => client.settle(paymentHeader, requirements)
651
+ scheme: routeConfig.scheme ?? "exact",
652
+ maxAmount: BigInt(requirements.maxAmountRequired),
653
+ decimals: resolveDecimals(routeConfig, asset),
654
+ handlerTimeoutMs: handlerTimeoutFor(routeConfig),
655
+ // Exact routes keep the historical two-argument client call.
656
+ settle: (options) => options ? client.settle(paymentHeader, requirements, options) : client.settle(paymentHeader, requirements),
657
+ ...routeConfig.meter && { meter: routeConfig.meter, request }
429
658
  };
430
659
  };
431
660
  }
@@ -437,14 +666,42 @@ function createPaymentMiddleware(config) {
437
666
  if (gateResult.type === "error") return gateResult.result;
438
667
  let handlerResult;
439
668
  try {
440
- handlerResult = await adapter.runHandler();
669
+ handlerResult = await adapter.runHandler({
670
+ timeoutMs: gateResult.handlerTimeoutMs
671
+ });
441
672
  } catch {
442
673
  return { pass: true, handlerRan: true };
443
674
  }
675
+ let raw;
676
+ if (gateResult.scheme === "upto") {
677
+ raw = handlerResult.getResponseHeader?.(SETTLE_AMOUNT_HEADER);
678
+ handlerResult.removeResponseHeader?.(SETTLE_AMOUNT_HEADER);
679
+ }
444
680
  if (handlerResult.status >= 200 && handlerResult.status < 300) {
681
+ let settleOptions;
682
+ if (gateResult.scheme === "upto") {
683
+ const resolved = await resolveCharge(gateResult, raw, {
684
+ status: handlerResult.status,
685
+ header: (name) => handlerResult.getResponseHeader?.(name),
686
+ body: async () => handlerResult.getResponseBody?.()
687
+ });
688
+ if (resolved.kind === "invalid") {
689
+ handlerResult.discardResponse();
690
+ return {
691
+ status: 402,
692
+ headers: { "Content-Type": "application/json" },
693
+ body: invalidSettleAmountResponse(resolved.reason)
694
+ };
695
+ }
696
+ if (resolved.kind === "skip") {
697
+ handlerResult.sendResponse();
698
+ return { pass: true, handlerRan: true };
699
+ }
700
+ settleOptions = { amount: resolved.amount };
701
+ }
445
702
  let settled = false;
446
703
  try {
447
- const settleResponse = await gateResult.settle();
704
+ const settleResponse = settleOptions ? await gateResult.settle(settleOptions) : await gateResult.settle();
448
705
  if (settleResponse.success) {
449
706
  handlerResult.setResponseHeader(
450
707
  "payment-response",
@@ -475,4 +732,4 @@ function createPaymentMiddleware(config) {
475
732
  };
476
733
  }
477
734
 
478
- export { FacilitatorClient, PARALLEL_TOKENS, X402ConfigError, X402RuntimeError, X402_ERROR_CODES, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, parsePrice, toEip155Network, validateAddress };
735
+ export { FacilitatorClient, HANDLER_TIMEOUT, PARALLEL_TOKENS, SETTLE_AMOUNT_HEADER, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, invalidSettleAmountResponse, isJsonContentType, jsonFromResponse, matchRoute, parseJsonBody, parsePrice, raceTimeout, resolveCharge, resolveSettleAmount, toEip155Network, validateAddress };
@@ -0,0 +1,36 @@
1
+ // src/errors.ts
2
+ var X402_ERROR_CODES = {
3
+ FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
4
+ FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
5
+ INVALID_PAYMENT: "INVALID_PAYMENT"
6
+ };
7
+ var X402ConfigError = class extends Error {
8
+ constructor(message, cause) {
9
+ super(message);
10
+ this.name = "X402ConfigError";
11
+ this.cause = cause;
12
+ }
13
+ };
14
+ var X402RuntimeError = class extends Error {
15
+ constructor(code) {
16
+ super(code);
17
+ this.code = code;
18
+ this.name = "X402RuntimeError";
19
+ }
20
+ code;
21
+ };
22
+ var X402PriceRejectedError = class extends Error {
23
+ constructor(code, message, status = 400) {
24
+ super(message);
25
+ this.code = code;
26
+ this.status = status;
27
+ this.name = "X402PriceRejectedError";
28
+ }
29
+ code;
30
+ status;
31
+ };
32
+ function isPriceRejection(error) {
33
+ return error instanceof Error && error.name === "X402PriceRejectedError" && typeof error.code === "string";
34
+ }
35
+
36
+ export { X402ConfigError, X402PriceRejectedError, X402RuntimeError, X402_ERROR_CODES, isPriceRejection };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var X402_ERROR_CODES = {
5
+ FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
6
+ FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
7
+ INVALID_PAYMENT: "INVALID_PAYMENT"
8
+ };
9
+ var X402ConfigError = class extends Error {
10
+ constructor(message, cause) {
11
+ super(message);
12
+ this.name = "X402ConfigError";
13
+ this.cause = cause;
14
+ }
15
+ };
16
+ var X402RuntimeError = class extends Error {
17
+ constructor(code) {
18
+ super(code);
19
+ this.code = code;
20
+ this.name = "X402RuntimeError";
21
+ }
22
+ code;
23
+ };
24
+ var X402PriceRejectedError = class extends Error {
25
+ constructor(code, message, status = 400) {
26
+ super(message);
27
+ this.code = code;
28
+ this.status = status;
29
+ this.name = "X402PriceRejectedError";
30
+ }
31
+ code;
32
+ status;
33
+ };
34
+ function isPriceRejection(error) {
35
+ return error instanceof Error && error.name === "X402PriceRejectedError" && typeof error.code === "string";
36
+ }
37
+
38
+ exports.X402ConfigError = X402ConfigError;
39
+ exports.X402PriceRejectedError = X402PriceRejectedError;
40
+ exports.X402RuntimeError = X402RuntimeError;
41
+ exports.X402_ERROR_CODES = X402_ERROR_CODES;
42
+ exports.isPriceRejection = isPriceRejection;