@parallel-protocol/x402 0.5.3 → 0.6.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.
package/README.md CHANGED
@@ -40,6 +40,7 @@ Agent Merchant (this SDK) Facilitator
40
40
  - The on-chain settlement happens after the handler produces a 2xx response — the agent is not charged on errors.
41
41
  - If settlement fails (on-chain revert, facilitator timeout), the middleware returns `402` and discards the handler's response.
42
42
  - Handlers are bounded by a 30-second timeout on Express, Hono and Next.js — a slow handler returns `504` and the agent is not charged. (Fastify manages handler execution itself, so no timeout is applied there.)
43
+ - On paid Fastify routes, writing to `reply.raw` (the SSE/streaming pattern) fails loudly instead of sending: a raw response skips the settlement hooks and would go out unpaid. Streaming is not supported on paid routes.
43
44
  - CORS preflight (`OPTIONS`) is always passed through without challenge.
44
45
 
45
46
  ---
@@ -198,6 +199,8 @@ interface PaymentMiddlewareConfig {
198
199
  | `payTo` | `Address` | Yes | The merchant's receiving address. |
199
200
  | `acceptedTokens` | `Address[]` | No | List of accepted token addresses. Defaults to `[USDp, USDC, sUSDp]` for known networks (see [Default tokens](#default-tokens)). |
200
201
  | `description` | `string` | No | Human-readable description of the resource, included in the 402 body. |
202
+ | `scheme` | `"exact" \| "upto"` | No | `exact` (default) charges `price` as invoiced. `upto` invoices `price` as a **cap** the payer signs; the handler names the actual amount to charge and the rest is refunded on-chain — see [Metered routes](#metered-routes-upto). |
203
+ | `maxTimeoutSeconds` | `number` | No | Settlement window the payer signs for (default 300). Metered routes announce how long their handler may take; the facilitator caps it at 3600 (one hour). Express, Hono and Next.js derive the handler timeout from it; Fastify does not time handlers out, set the server's `connectionTimeout` / `requestTimeout` yourself. |
201
204
 
202
205
  Route matching uses **longest prefix wins**: `/api/v1/users` will match a `/api/v1` route config before a `/api` one.
203
206
 
@@ -299,12 +302,12 @@ interface PaymentRequired {
299
302
  mimeType: "application/json";
300
303
  };
301
304
  accepts: Array<{ // one entry per accepted token
302
- scheme: "exact";
305
+ scheme: "exact" | "upto"; // the route's scheme ("exact" unless configured)
303
306
  network: string; // EIP-155 format: "eip155:8453"
304
307
  asset: string; // token address
305
- amount: string; // smallest unit of `asset`, per its own decimals
308
+ amount: string; // smallest unit of `asset`, per its own decimals — the cap for "upto"
306
309
  payTo: string;
307
- maxTimeoutSeconds: 300;
310
+ maxTimeoutSeconds: number; // the route's settlement window (default 300)
308
311
  extra: { decimals: number }; // decimals of `asset` — payers need not guess
309
312
  }>;
310
313
  }
@@ -350,6 +353,7 @@ interface PaymentConfirmation {
350
353
  chain: string; // "base"
351
354
  gasSponsored: boolean;
352
355
  networkId: string; // "eip155:8453"
356
+ amountSettled?: string; // metered routes: what was actually charged (smallest unit)
353
357
  }
354
358
  ```
355
359
 
@@ -357,9 +361,51 @@ The `access-control-expose-headers` header is set automatically so browsers can
357
361
 
358
362
  ---
359
363
 
364
+ ## Metered routes (`upto`)
365
+
366
+ Some resources only know their cost after the work is done — an LLM completion is priced by the tokens it generates. A route declared with `scheme: "upto"` invoices `price` as a **cap**: the payer signs an authorization for that cap, the handler serves the request and names the amount it actually charges, and the facilitator settles that amount through Parallel's escrow contract, refunding the remainder to the payer in the same transaction. Metered payments are made in USDp.
367
+
368
+ ```typescript
369
+ routes: {
370
+ "/v1/chat/completions": {
371
+ scheme: "upto",
372
+ price: "2", // the cap, in USDp
373
+ maxTimeoutSeconds: 600, // the handler may take up to ~9 minutes
374
+ network: "base",
375
+ payTo: "0xYourAddress",
376
+ acceptedTokens: [PARALLEL_TOKENS.base.usdp],
377
+ },
378
+ },
379
+ ```
380
+
381
+ The handler reports the amount to charge through the `payment-settle-amount` response header, in whole token units like `price`:
382
+
383
+ ```typescript
384
+ import { SETTLE_AMOUNT_HEADER } from "@parallel-protocol/x402";
385
+
386
+ fastify.post("/v1/chat/completions", async (request, reply) => {
387
+ const completion = await runInference(request.body);
388
+ reply.header(SETTLE_AMOUNT_HEADER, completion.costInUsdp); // e.g. "0.0137"
389
+ return completion;
390
+ });
391
+ ```
392
+
393
+ The middleware strips the header before the response leaves, whatever the status code: a handler that prices the request and then fails never tells the client what it was about to charge. The value must follow the same format as `price` (a plain decimal such as `"0.0137"`, no leading zeros on the integer part, no exponent, no unit) and must not carry more decimals than the token has. Rules on a `2xx` response:
394
+
395
+ | Header value | What happens |
396
+ |---|---|
397
+ | absent | The cap is charged. |
398
+ | `"0"` (an explicit zero) | The request is served for free — nothing is settled, no `payment-response`. |
399
+ | a decimal up to `price` | That amount is settled; the receipt's `amountSettled` carries it. |
400
+ | malformed, more decimals than the token, or above `price` | The response is discarded and the client gets 402 `INVALID_SETTLE_AMOUNT` — a handler bug never charges the payer, and a positive amount the token cannot represent is never silently served for free. Round your cost to the token's precision before setting the header. |
401
+
402
+ Timing: the payer's authorization expires after `maxTimeoutSeconds`, and settlement needs a minute at most. On Express, Hono and Next.js the handler timeout is raised to `maxTimeoutSeconds − 60 s` for metered routes; Fastify handlers are never timed out by the middleware, so size the server's own `connectionTimeout` and `requestTimeout` to the window. A handler that finishes after the window is served but cannot be settled: the client gets 402 `PAYMENT_SETTLEMENT_FAILED` and the merchant has done the work for nothing — keep a margin.
403
+
404
+ ---
405
+
360
406
  ## Payment routes
361
407
 
362
- The facilitator supports 12 settlement routes. The route is selected automatically based on the signed payload:
408
+ The facilitator supports 13 settlement routes. The route is selected automatically based on the signed payload:
363
409
 
364
410
  | Route | Credential method(s) | tokenIn (agent pays) | tokenOut (merchant receives) | Gas est. | Notes |
365
411
  |-------|--------------------------|---------------------|------------------------------|----------|-------|
@@ -375,6 +421,7 @@ The facilitator supports 12 settlement routes. The route is selected automatical
375
421
  | **J** | `transferWithAuthorization` | sUSDp | sUSDp | ~65K | Direct sUSDp share transfer |
376
422
  | **K** | `redeemWithAuthorization` + `swapExactOutputWithAuthorization` | sUSDp | non-USDC backing (exact) | ~180K | Same as I but for other Parallelizer collaterals (atomic multicall3) |
377
423
  | **L** | `partialRedeemWithAuthorization` | sUSDp (partial) + USDp | USDp or backing collateral | ~180K | Partial sUSDp redeem combined with existing USDp; delivers to merchant (atomic multicall3) |
424
+ | **U** | `uptoWithAuthorization` | USDp (cap) | USDp (actual) | ~130K | Metered payment: the escrow pulls the signed cap, pays the merchant the settled amount and refunds the rest atomically (scheme `upto`) |
378
425
 
379
426
  **Chain requirements:**
380
427
  - Routes B, D, E, G, I, K, L require the **Parallelizer** to be deployed on the target chain (Ethereum, Base, Avalanche, HyperEVM).
@@ -82,13 +82,19 @@ var facilitatorConfigSchema = zod.z.object({
82
82
  url: zod.z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
83
83
  apiKey: zod.z.string().optional()
84
84
  });
85
+ var UPTO_MAX_TIMEOUT_SECONDS = 3600;
85
86
  var routeConfigSchema = zod.z.object({
86
87
  price: priceSchema,
87
88
  network: zod.z.string().min(1, "Network is required"),
88
89
  payTo: ethereumAddressSchema,
89
90
  acceptedTokens: zod.z.array(ethereumAddressSchema).optional(),
90
- description: zod.z.string().optional()
91
- });
91
+ description: zod.z.string().optional(),
92
+ scheme: zod.z.enum(["exact", "upto"]).optional(),
93
+ maxTimeoutSeconds: zod.z.number().int().positive().optional()
94
+ }).refine(
95
+ (route) => route.scheme !== "upto" || (route.maxTimeoutSeconds ?? 300) <= UPTO_MAX_TIMEOUT_SECONDS,
96
+ `maxTimeoutSeconds must be at most ${UPTO_MAX_TIMEOUT_SECONDS} on an upto route`
97
+ );
92
98
  var paymentMiddlewareConfigSchema = zod.z.object({
93
99
  facilitator: facilitatorConfigSchema,
94
100
  routes: zod.z.record(zod.z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
@@ -100,7 +106,8 @@ var facilitatorResponseSchema = zod.z.discriminatedUnion("success", [
100
106
  route: zod.z.string(),
101
107
  chain: zod.z.string(),
102
108
  gasSponsored: zod.z.boolean(),
103
- networkId: zod.z.string()
109
+ networkId: zod.z.string(),
110
+ amountSettled: zod.z.string().optional()
104
111
  }),
105
112
  zod.z.object({
106
113
  success: zod.z.literal(false),
@@ -117,7 +124,9 @@ var verifyResponseSchema = zod.z.object({
117
124
  var settleSuccessSchema = zod.z.object({
118
125
  txHash: txHashSchema,
119
126
  route: zod.z.string(),
120
- gasSponsored: zod.z.boolean()
127
+ gasSponsored: zod.z.boolean(),
128
+ // Metered routes: the amount actually charged (smallest unit).
129
+ amountSettled: zod.z.string().optional()
121
130
  });
122
131
  var settleErrorSchema = zod.z.object({
123
132
  error: zod.z.string(),
@@ -169,14 +178,21 @@ var FacilitatorClient = class {
169
178
  throw new X402RuntimeError(X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
170
179
  }
171
180
  }
172
- async settle(paymentHeader, paymentRequirements) {
181
+ async settle(paymentHeader, paymentRequirements, options) {
173
182
  const paymentPayload = decodePaymentHeader(paymentHeader);
174
183
  let json;
175
184
  try {
176
185
  const response = await fetch(`${this.config.url}/x402/settle`, {
177
186
  method: "POST",
178
187
  headers: this.buildHeaders(),
179
- body: JSON.stringify({ paymentPayload, paymentRequirements }),
188
+ // settleAmount only travels for metered routes — exact bodies are unchanged.
189
+ body: JSON.stringify({
190
+ paymentPayload,
191
+ paymentRequirements,
192
+ ...options?.amount !== void 0 && {
193
+ settleAmount: options.amount
194
+ }
195
+ }),
180
196
  signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
181
197
  });
182
198
  if (!response.ok) {
@@ -199,7 +215,10 @@ var FacilitatorClient = class {
199
215
  route: parsed.route,
200
216
  chain: paymentRequirements.network,
201
217
  gasSponsored: parsed.gasSponsored,
202
- networkId: toEip155Network(paymentRequirements.network)
218
+ networkId: toEip155Network(paymentRequirements.network),
219
+ ...parsed.amountSettled !== void 0 && {
220
+ amountSettled: parsed.amountSettled
221
+ }
203
222
  };
204
223
  }
205
224
  return {
@@ -244,6 +263,22 @@ var FacilitatorClient = class {
244
263
  return headers;
245
264
  }
246
265
  };
266
+
267
+ // src/timeout.ts
268
+ var HANDLER_TIMEOUT_MS = 3e4;
269
+ var HANDLER_TIMEOUT = /* @__PURE__ */ Symbol("x402:handler-timeout");
270
+ async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
271
+ let timer;
272
+ const timeout = new Promise((resolve) => {
273
+ timer = setTimeout(() => resolve(HANDLER_TIMEOUT), ms);
274
+ timer.unref?.();
275
+ });
276
+ try {
277
+ return await Promise.race([promise, timeout]);
278
+ } finally {
279
+ clearTimeout(timer);
280
+ }
281
+ }
247
282
  var PARALLEL_TOKENS = {
248
283
  ethereum: {
249
284
  usdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
@@ -285,6 +320,34 @@ function getTokenDecimals(network, address) {
285
320
  }
286
321
 
287
322
  // src/middleware.ts
323
+ var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
324
+ var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
325
+ function handlerTimeoutFor(config) {
326
+ if ((config.scheme ?? "exact") !== "upto") return HANDLER_TIMEOUT_MS;
327
+ const seconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
328
+ return Math.max(HANDLER_TIMEOUT_MS, (seconds - 60) * 1e3);
329
+ }
330
+ function resolveSettleAmount(raw, maxAmount, decimals) {
331
+ if (raw === void 0)
332
+ return { kind: "settle", amount: maxAmount.toString() };
333
+ const value = raw.trim();
334
+ let amount;
335
+ try {
336
+ amount = parsePrice(value, decimals);
337
+ } catch {
338
+ return { kind: "invalid", reason: "malformed" };
339
+ }
340
+ const fraction = value.split(".")[1] ?? "";
341
+ if (fraction.length > decimals)
342
+ return { kind: "invalid", reason: "precision" };
343
+ if (amount === 0n) return { kind: "skip" };
344
+ if (amount > maxAmount) return { kind: "invalid", reason: "above-cap" };
345
+ return { kind: "settle", amount: amount.toString() };
346
+ }
347
+ var INVALID_SETTLE_AMOUNT_RESPONSE = {
348
+ error: "INVALID_SETTLE_AMOUNT",
349
+ 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.`
350
+ };
288
351
  function resolveAcceptedTokens(config) {
289
352
  if (config.acceptedTokens?.length) {
290
353
  return config.acceptedTokens;
@@ -309,12 +372,12 @@ function buildPaymentRequired(url, config, resolvedTokens) {
309
372
  const accepts = resolvedTokens.map((asset) => {
310
373
  const decimals = resolveDecimals(config, asset);
311
374
  return {
312
- scheme: "exact",
375
+ scheme: config.scheme ?? "exact",
313
376
  network,
314
377
  asset,
315
378
  amount: parsePrice(config.price, decimals).toString(),
316
379
  payTo: config.payTo,
317
- maxTimeoutSeconds: 300,
380
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
318
381
  extra: { decimals }
319
382
  };
320
383
  });
@@ -360,11 +423,17 @@ function resolveDeliveredAsset(header, config, resolvedTokens) {
360
423
  }
361
424
  function buildFacilitatorRequirements(config, asset) {
362
425
  const decimals = resolveDecimals(config, asset);
426
+ const scheme = config.scheme ?? "exact";
363
427
  return {
364
428
  maxAmountRequired: parsePrice(config.price, decimals).toString(),
365
429
  asset,
366
430
  payTo: config.payTo,
367
- network: config.network
431
+ network: config.network,
432
+ // Only metered routes name their scheme — exact requirements are unchanged.
433
+ ...scheme === "upto" && {
434
+ scheme,
435
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS
436
+ }
368
437
  };
369
438
  }
370
439
  function paymentRequiredResponse(url, config, resolvedTokens, error) {
@@ -407,10 +476,12 @@ function createPaymentGate(config) {
407
476
  )
408
477
  };
409
478
  }
410
- const requirements = buildFacilitatorRequirements(
479
+ const asset = resolveDeliveredAsset(
480
+ paymentHeader,
411
481
  routeConfig,
412
- resolveDeliveredAsset(paymentHeader, routeConfig, resolvedTokens)
482
+ resolvedTokens
413
483
  );
484
+ const requirements = buildFacilitatorRequirements(routeConfig, asset);
414
485
  try {
415
486
  await client.verify(paymentHeader, requirements);
416
487
  } catch (e) {
@@ -427,7 +498,12 @@ function createPaymentGate(config) {
427
498
  }
428
499
  return {
429
500
  type: "verified",
430
- settle: () => client.settle(paymentHeader, requirements)
501
+ scheme: routeConfig.scheme ?? "exact",
502
+ maxAmount: BigInt(requirements.maxAmountRequired),
503
+ decimals: resolveDecimals(routeConfig, asset),
504
+ handlerTimeoutMs: handlerTimeoutFor(routeConfig),
505
+ // Exact routes keep the historical two-argument client call.
506
+ settle: (options) => options ? client.settle(paymentHeader, requirements, options) : client.settle(paymentHeader, requirements)
431
507
  };
432
508
  };
433
509
  }
@@ -439,14 +515,42 @@ function createPaymentMiddleware(config) {
439
515
  if (gateResult.type === "error") return gateResult.result;
440
516
  let handlerResult;
441
517
  try {
442
- handlerResult = await adapter.runHandler();
518
+ handlerResult = await adapter.runHandler({
519
+ timeoutMs: gateResult.handlerTimeoutMs
520
+ });
443
521
  } catch {
444
522
  return { pass: true, handlerRan: true };
445
523
  }
524
+ let raw;
525
+ if (gateResult.scheme === "upto") {
526
+ raw = handlerResult.getResponseHeader?.(SETTLE_AMOUNT_HEADER);
527
+ handlerResult.removeResponseHeader?.(SETTLE_AMOUNT_HEADER);
528
+ }
446
529
  if (handlerResult.status >= 200 && handlerResult.status < 300) {
530
+ let settleOptions;
531
+ if (gateResult.scheme === "upto") {
532
+ const resolved = resolveSettleAmount(
533
+ raw,
534
+ gateResult.maxAmount,
535
+ gateResult.decimals
536
+ );
537
+ if (resolved.kind === "invalid") {
538
+ handlerResult.discardResponse();
539
+ return {
540
+ status: 402,
541
+ headers: { "Content-Type": "application/json" },
542
+ body: INVALID_SETTLE_AMOUNT_RESPONSE
543
+ };
544
+ }
545
+ if (resolved.kind === "skip") {
546
+ handlerResult.sendResponse();
547
+ return { pass: true, handlerRan: true };
548
+ }
549
+ settleOptions = { amount: resolved.amount };
550
+ }
447
551
  let settled = false;
448
552
  try {
449
- const settleResponse = await gateResult.settle();
553
+ const settleResponse = settleOptions ? await gateResult.settle(settleOptions) : await gateResult.settle();
450
554
  if (settleResponse.success) {
451
555
  handlerResult.setResponseHeader(
452
556
  "payment-response",
@@ -478,7 +582,10 @@ function createPaymentMiddleware(config) {
478
582
  }
479
583
 
480
584
  exports.FacilitatorClient = FacilitatorClient;
585
+ exports.HANDLER_TIMEOUT = HANDLER_TIMEOUT;
586
+ exports.INVALID_SETTLE_AMOUNT_RESPONSE = INVALID_SETTLE_AMOUNT_RESPONSE;
481
587
  exports.PARALLEL_TOKENS = PARALLEL_TOKENS;
588
+ exports.SETTLE_AMOUNT_HEADER = SETTLE_AMOUNT_HEADER;
482
589
  exports.X402ConfigError = X402ConfigError;
483
590
  exports.X402RuntimeError = X402RuntimeError;
484
591
  exports.X402_ERROR_CODES = X402_ERROR_CODES;
@@ -487,5 +594,7 @@ exports.createPaymentMiddleware = createPaymentMiddleware;
487
594
  exports.encodeBase64 = encodeBase64;
488
595
  exports.getDefaultAcceptedTokens = getDefaultAcceptedTokens;
489
596
  exports.parsePrice = parsePrice;
597
+ exports.raceTimeout = raceTimeout;
598
+ exports.resolveSettleAmount = resolveSettleAmount;
490
599
  exports.toEip155Network = toEip155Network;
491
600
  exports.validateAddress = validateAddress;
@@ -80,13 +80,19 @@ var facilitatorConfigSchema = z.object({
80
80
  url: z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
81
81
  apiKey: z.string().optional()
82
82
  });
83
+ var UPTO_MAX_TIMEOUT_SECONDS = 3600;
83
84
  var routeConfigSchema = z.object({
84
85
  price: priceSchema,
85
86
  network: z.string().min(1, "Network is required"),
86
87
  payTo: ethereumAddressSchema,
87
88
  acceptedTokens: z.array(ethereumAddressSchema).optional(),
88
- description: z.string().optional()
89
- });
89
+ description: z.string().optional(),
90
+ scheme: z.enum(["exact", "upto"]).optional(),
91
+ maxTimeoutSeconds: z.number().int().positive().optional()
92
+ }).refine(
93
+ (route) => route.scheme !== "upto" || (route.maxTimeoutSeconds ?? 300) <= UPTO_MAX_TIMEOUT_SECONDS,
94
+ `maxTimeoutSeconds must be at most ${UPTO_MAX_TIMEOUT_SECONDS} on an upto route`
95
+ );
90
96
  var paymentMiddlewareConfigSchema = z.object({
91
97
  facilitator: facilitatorConfigSchema,
92
98
  routes: z.record(z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
@@ -98,7 +104,8 @@ var facilitatorResponseSchema = z.discriminatedUnion("success", [
98
104
  route: z.string(),
99
105
  chain: z.string(),
100
106
  gasSponsored: z.boolean(),
101
- networkId: z.string()
107
+ networkId: z.string(),
108
+ amountSettled: z.string().optional()
102
109
  }),
103
110
  z.object({
104
111
  success: z.literal(false),
@@ -115,7 +122,9 @@ var verifyResponseSchema = z.object({
115
122
  var settleSuccessSchema = z.object({
116
123
  txHash: txHashSchema,
117
124
  route: z.string(),
118
- gasSponsored: z.boolean()
125
+ gasSponsored: z.boolean(),
126
+ // Metered routes: the amount actually charged (smallest unit).
127
+ amountSettled: z.string().optional()
119
128
  });
120
129
  var settleErrorSchema = z.object({
121
130
  error: z.string(),
@@ -167,14 +176,21 @@ var FacilitatorClient = class {
167
176
  throw new X402RuntimeError(X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
168
177
  }
169
178
  }
170
- async settle(paymentHeader, paymentRequirements) {
179
+ async settle(paymentHeader, paymentRequirements, options) {
171
180
  const paymentPayload = decodePaymentHeader(paymentHeader);
172
181
  let json;
173
182
  try {
174
183
  const response = await fetch(`${this.config.url}/x402/settle`, {
175
184
  method: "POST",
176
185
  headers: this.buildHeaders(),
177
- body: JSON.stringify({ paymentPayload, paymentRequirements }),
186
+ // settleAmount only travels for metered routes — exact bodies are unchanged.
187
+ body: JSON.stringify({
188
+ paymentPayload,
189
+ paymentRequirements,
190
+ ...options?.amount !== void 0 && {
191
+ settleAmount: options.amount
192
+ }
193
+ }),
178
194
  signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
179
195
  });
180
196
  if (!response.ok) {
@@ -197,7 +213,10 @@ var FacilitatorClient = class {
197
213
  route: parsed.route,
198
214
  chain: paymentRequirements.network,
199
215
  gasSponsored: parsed.gasSponsored,
200
- networkId: toEip155Network(paymentRequirements.network)
216
+ networkId: toEip155Network(paymentRequirements.network),
217
+ ...parsed.amountSettled !== void 0 && {
218
+ amountSettled: parsed.amountSettled
219
+ }
201
220
  };
202
221
  }
203
222
  return {
@@ -242,6 +261,22 @@ var FacilitatorClient = class {
242
261
  return headers;
243
262
  }
244
263
  };
264
+
265
+ // src/timeout.ts
266
+ var HANDLER_TIMEOUT_MS = 3e4;
267
+ var HANDLER_TIMEOUT = /* @__PURE__ */ Symbol("x402:handler-timeout");
268
+ async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
269
+ let timer;
270
+ const timeout = new Promise((resolve) => {
271
+ timer = setTimeout(() => resolve(HANDLER_TIMEOUT), ms);
272
+ timer.unref?.();
273
+ });
274
+ try {
275
+ return await Promise.race([promise, timeout]);
276
+ } finally {
277
+ clearTimeout(timer);
278
+ }
279
+ }
245
280
  var PARALLEL_TOKENS = {
246
281
  ethereum: {
247
282
  usdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
@@ -283,6 +318,34 @@ function getTokenDecimals(network, address) {
283
318
  }
284
319
 
285
320
  // src/middleware.ts
321
+ var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
322
+ var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
323
+ function handlerTimeoutFor(config) {
324
+ if ((config.scheme ?? "exact") !== "upto") return HANDLER_TIMEOUT_MS;
325
+ const seconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
326
+ return Math.max(HANDLER_TIMEOUT_MS, (seconds - 60) * 1e3);
327
+ }
328
+ function resolveSettleAmount(raw, maxAmount, decimals) {
329
+ if (raw === void 0)
330
+ return { kind: "settle", amount: maxAmount.toString() };
331
+ const value = raw.trim();
332
+ let amount;
333
+ try {
334
+ amount = parsePrice(value, decimals);
335
+ } catch {
336
+ return { kind: "invalid", reason: "malformed" };
337
+ }
338
+ const fraction = value.split(".")[1] ?? "";
339
+ if (fraction.length > decimals)
340
+ return { kind: "invalid", reason: "precision" };
341
+ if (amount === 0n) return { kind: "skip" };
342
+ if (amount > maxAmount) return { kind: "invalid", reason: "above-cap" };
343
+ return { kind: "settle", amount: amount.toString() };
344
+ }
345
+ var INVALID_SETTLE_AMOUNT_RESPONSE = {
346
+ error: "INVALID_SETTLE_AMOUNT",
347
+ 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.`
348
+ };
286
349
  function resolveAcceptedTokens(config) {
287
350
  if (config.acceptedTokens?.length) {
288
351
  return config.acceptedTokens;
@@ -307,12 +370,12 @@ function buildPaymentRequired(url, config, resolvedTokens) {
307
370
  const accepts = resolvedTokens.map((asset) => {
308
371
  const decimals = resolveDecimals(config, asset);
309
372
  return {
310
- scheme: "exact",
373
+ scheme: config.scheme ?? "exact",
311
374
  network,
312
375
  asset,
313
376
  amount: parsePrice(config.price, decimals).toString(),
314
377
  payTo: config.payTo,
315
- maxTimeoutSeconds: 300,
378
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
316
379
  extra: { decimals }
317
380
  };
318
381
  });
@@ -358,11 +421,17 @@ function resolveDeliveredAsset(header, config, resolvedTokens) {
358
421
  }
359
422
  function buildFacilitatorRequirements(config, asset) {
360
423
  const decimals = resolveDecimals(config, asset);
424
+ const scheme = config.scheme ?? "exact";
361
425
  return {
362
426
  maxAmountRequired: parsePrice(config.price, decimals).toString(),
363
427
  asset,
364
428
  payTo: config.payTo,
365
- network: config.network
429
+ network: config.network,
430
+ // Only metered routes name their scheme — exact requirements are unchanged.
431
+ ...scheme === "upto" && {
432
+ scheme,
433
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS
434
+ }
366
435
  };
367
436
  }
368
437
  function paymentRequiredResponse(url, config, resolvedTokens, error) {
@@ -405,10 +474,12 @@ function createPaymentGate(config) {
405
474
  )
406
475
  };
407
476
  }
408
- const requirements = buildFacilitatorRequirements(
477
+ const asset = resolveDeliveredAsset(
478
+ paymentHeader,
409
479
  routeConfig,
410
- resolveDeliveredAsset(paymentHeader, routeConfig, resolvedTokens)
480
+ resolvedTokens
411
481
  );
482
+ const requirements = buildFacilitatorRequirements(routeConfig, asset);
412
483
  try {
413
484
  await client.verify(paymentHeader, requirements);
414
485
  } catch (e) {
@@ -425,7 +496,12 @@ function createPaymentGate(config) {
425
496
  }
426
497
  return {
427
498
  type: "verified",
428
- settle: () => client.settle(paymentHeader, requirements)
499
+ scheme: routeConfig.scheme ?? "exact",
500
+ maxAmount: BigInt(requirements.maxAmountRequired),
501
+ decimals: resolveDecimals(routeConfig, asset),
502
+ handlerTimeoutMs: handlerTimeoutFor(routeConfig),
503
+ // Exact routes keep the historical two-argument client call.
504
+ settle: (options) => options ? client.settle(paymentHeader, requirements, options) : client.settle(paymentHeader, requirements)
429
505
  };
430
506
  };
431
507
  }
@@ -437,14 +513,42 @@ function createPaymentMiddleware(config) {
437
513
  if (gateResult.type === "error") return gateResult.result;
438
514
  let handlerResult;
439
515
  try {
440
- handlerResult = await adapter.runHandler();
516
+ handlerResult = await adapter.runHandler({
517
+ timeoutMs: gateResult.handlerTimeoutMs
518
+ });
441
519
  } catch {
442
520
  return { pass: true, handlerRan: true };
443
521
  }
522
+ let raw;
523
+ if (gateResult.scheme === "upto") {
524
+ raw = handlerResult.getResponseHeader?.(SETTLE_AMOUNT_HEADER);
525
+ handlerResult.removeResponseHeader?.(SETTLE_AMOUNT_HEADER);
526
+ }
444
527
  if (handlerResult.status >= 200 && handlerResult.status < 300) {
528
+ let settleOptions;
529
+ if (gateResult.scheme === "upto") {
530
+ const resolved = resolveSettleAmount(
531
+ raw,
532
+ gateResult.maxAmount,
533
+ gateResult.decimals
534
+ );
535
+ if (resolved.kind === "invalid") {
536
+ handlerResult.discardResponse();
537
+ return {
538
+ status: 402,
539
+ headers: { "Content-Type": "application/json" },
540
+ body: INVALID_SETTLE_AMOUNT_RESPONSE
541
+ };
542
+ }
543
+ if (resolved.kind === "skip") {
544
+ handlerResult.sendResponse();
545
+ return { pass: true, handlerRan: true };
546
+ }
547
+ settleOptions = { amount: resolved.amount };
548
+ }
445
549
  let settled = false;
446
550
  try {
447
- const settleResponse = await gateResult.settle();
551
+ const settleResponse = settleOptions ? await gateResult.settle(settleOptions) : await gateResult.settle();
448
552
  if (settleResponse.success) {
449
553
  handlerResult.setResponseHeader(
450
554
  "payment-response",
@@ -475,4 +579,4 @@ function createPaymentMiddleware(config) {
475
579
  };
476
580
  }
477
581
 
478
- export { FacilitatorClient, PARALLEL_TOKENS, X402ConfigError, X402RuntimeError, X402_ERROR_CODES, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, parsePrice, toEip155Network, validateAddress };
582
+ export { FacilitatorClient, HANDLER_TIMEOUT, INVALID_SETTLE_AMOUNT_RESPONSE, PARALLEL_TOKENS, SETTLE_AMOUNT_HEADER, X402ConfigError, X402RuntimeError, X402_ERROR_CODES, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, parsePrice, raceTimeout, resolveSettleAmount, toEip155Network, validateAddress };
package/dist/express.cjs CHANGED
@@ -1,9 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var chunkXUNNMCO5_cjs = require('./chunk-XUNNMCO5.cjs');
4
- var chunkKQPIZ6SZ_cjs = require('./chunk-KQPIZ6SZ.cjs');
3
+ var chunkK6QETMQA_cjs = require('./chunk-K6QETMQA.cjs');
5
4
 
6
5
  // src/express/adapter.ts
6
+ function headerValue(value) {
7
+ if (value === void 0) return void 0;
8
+ return Array.isArray(value) ? value[0] : String(value);
9
+ }
7
10
  var TIMEOUT_BODY = JSON.stringify({
8
11
  error: "HANDLER_TIMEOUT",
9
12
  message: "The resource handler did not respond in time."
@@ -30,7 +33,7 @@ var ExpressAdapter = class {
30
33
  getUrl() {
31
34
  return `${this.req.protocol}://${this.req.headers.host}${this.req.originalUrl}`;
32
35
  }
33
- async runHandler() {
36
+ async runHandler(options) {
34
37
  const res = this.res;
35
38
  const next = this.next;
36
39
  const originalWriteHead = res.writeHead.bind(res);
@@ -74,14 +77,17 @@ var ExpressAdapter = class {
74
77
  return originalEnd(...args);
75
78
  });
76
79
  next();
77
- const outcome = await chunkXUNNMCO5_cjs.raceTimeout(endPromise);
78
- if (outcome === chunkXUNNMCO5_cjs.HANDLER_TIMEOUT) {
80
+ const outcome = await chunkK6QETMQA_cjs.raceTimeout(endPromise, options?.timeoutMs);
81
+ if (outcome === chunkK6QETMQA_cjs.HANDLER_TIMEOUT) {
79
82
  discarded = true;
80
83
  return {
81
84
  status: 504,
82
85
  setResponseHeader: (name, value) => {
83
86
  if (!res.headersSent) res.setHeader(name, value);
84
87
  },
88
+ getResponseHeader: () => void 0,
89
+ removeResponseHeader: () => {
90
+ },
85
91
  sendResponse: () => {
86
92
  if (res.headersSent) return;
87
93
  originalWriteHead(504, { "Content-Type": "application/json" });
@@ -94,6 +100,8 @@ var ExpressAdapter = class {
94
100
  return {
95
101
  status: res.statusCode,
96
102
  setResponseHeader: (name, value) => res.setHeader(name, value),
103
+ getResponseHeader: (name) => headerValue(res.getHeader(name)),
104
+ removeResponseHeader: (name) => res.removeHeader(name),
97
105
  sendResponse: () => {
98
106
  settled = true;
99
107
  restore();
@@ -113,7 +121,7 @@ var ExpressAdapter = class {
113
121
 
114
122
  // src/express/index.ts
115
123
  function paymentMiddleware(config) {
116
- const middleware = chunkKQPIZ6SZ_cjs.createPaymentMiddleware(config);
124
+ const middleware = chunkK6QETMQA_cjs.createPaymentMiddleware(config);
117
125
  return async (req, res, next) => {
118
126
  const result = await middleware(new ExpressAdapter(req, res, next));
119
127
  if ("pass" in result) {