@parallel-protocol/x402 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -104,9 +104,10 @@ app.get("/api/data", (req, res) => {
104
104
  ```typescript
105
105
  // app/api/data/route.ts
106
106
  import { NextResponse } from "next/server";
107
+ import type { PaymentMiddlewareConfig } from "@parallel-protocol/x402";
107
108
  import { withPayment } from "@parallel-protocol/x402/next";
108
109
 
109
- const config = {
110
+ const config: PaymentMiddlewareConfig = {
110
111
  facilitator: { url: "https://agents.parallel.best" },
111
112
  routes: {
112
113
  "/api/data": {
@@ -189,7 +190,7 @@ interface PaymentMiddlewareConfig {
189
190
 
190
191
  | Field | Type | Required | Description |
191
192
  |-------|------|----------|-------------|
192
- | `url` | `string` | Yes | Base URL of the Parallel facilitator (e.g. `https://agents.parallel.best`). The SDK internally appends `/x402/verify` and `/x402/settle` to this value. |
193
+ | `url` | `string` | Yes | Base URL of the Parallel facilitator (e.g. `https://agents.parallel.best`). The SDK internally appends `/x402/verify` and `/x402/settle` to this value. It must use https: plain http is accepted only for `localhost`, `127.0.0.1` and `[::1]`. A trailing slash is ignored. The SDK calls this address only and never follows a redirect, which fails the call with `FACILITATOR_UNAVAILABLE`. |
193
194
  | `apiKey` | `string` | No | Optional API key, sent as the `X-API-Key` header on facilitator requests. |
194
195
 
195
196
  ### `RouteConfig`
@@ -197,14 +198,14 @@ interface PaymentMiddlewareConfig {
197
198
  | Field | Type | Required | Description |
198
199
  |-------|------|----------|-------------|
199
200
  | `price` | `string \| bigint \| (ctx) => string \| bigint` | Yes | Amount required. A string like `"0.01"` is parsed with each accepted token's own decimals (or with `decimals` when set explicitly). Pass a `bigint` to skip parsing. A function prices each request from its content, see [Per-request pricing](#per-request-pricing). |
200
- | `decimals` | `number` | No | Override for the decimals used to parse a string price. By default each accepted token is priced with its own decimals from the `@parallel-protocol/chains` catalog — a `"0.01"` price is correct whether paid in USDp (18) or USDC (6), no configuration. Only needed for a custom token outside the catalog; without it, an unknown token raises `X402ConfigError` rather than being silently guessed. |
201
+ | `decimals` | `number` | No | Override for the decimals used to parse a string price. By default each accepted token is priced with its own decimals from the `@parallel-protocol/chains` catalog, so a `"0.01"` price is correct whether paid in USDp (18) or USDC (6), with no configuration. Only needed for a custom token outside the catalog: without it, an unknown token raises `X402ConfigError` rather than being silently guessed. When set, it applies to every token of the route. |
201
202
  | `network` | `string` | Yes | Chain slug: `"ethereum"`, `"base"`, `"avalanche"`, `"hyperevm"`. |
202
203
  | `payTo` | `Address` | Yes | The merchant's receiving address. |
203
204
  | `acceptedTokens` | `Address[]` | No | List of accepted token addresses. Defaults to `[USDp, USDC, sUSDp]` for known networks (see [Default tokens](#default-tokens)). On an `upto` route it defaults to USDp alone, and any other token is rejected at construction: metered payments are USDp only. |
204
205
  | `description` | `string` | No | Human-readable description of the resource, included in the 402 body. |
205
206
  | `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). |
206
207
  | `meter` | `(ctx) => string \| bigint \| undefined` | No | `upto` routes only: reads the amount to charge from the handler's response, see [Reading the amount from the response](#reading-the-amount-from-the-response-meter). |
207
- | `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). On metered routes, Express, Hono and Next.js derive the handler timeout from it, minus two minutes kept for the settlement and its retry: a 300 s window leaves the handler 180 s. Fastify does not time handlers out, set the server's `connectionTimeout` / `requestTimeout` yourself. |
208
+ | `maxTimeoutSeconds` | `number` | No | Settlement window announced to the payer (default 300). `@parallel-protocol/x402-fetch` signs for it on `upto` routes, and for five minutes on `exact` routes. Metered routes announce how long their handler may take. On an `upto` route it is 3600 (one hour) at most: a larger value raises `X402ConfigError` at construction, and the facilitator refuses an authorization valid for longer with `WINDOW_TOO_LONG`. On metered routes, Express, Hono and Next.js derive the handler timeout from it, minus two minutes kept for the settlement and its retry: a 300 s window leaves the handler 180 s, and a handler always gets at least 30 s. Fastify does not time handlers out, set the server's `connectionTimeout` / `requestTimeout` yourself. |
208
209
 
209
210
  Route matching uses **longest prefix wins**: `/api/v1/users` will match a `/api/v1` route config before a `/api` one.
210
211
 
@@ -221,7 +222,7 @@ For the four natively supported networks, `acceptedTokens` defaults to `[USDp, U
221
222
  | `avalanche` | `0x9eE1963f05553eF838604Dd39403be21ceF26AA4` | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` | `0x9d92c21205383651610f90722131655a5b8ed3e0` |
222
223
  | `hyperevm` | `0xBE65F0F410A72BeC163dC65d46c83699e957D588` | `0xb88339cb7199b77e23db6e890353e22632ba630f` | `0x9B3a8f7CEC208e247d97dEE13313690977e24459` |
223
224
 
224
- For any other network (from the full Parallel 25-chain catalog), you must provide `acceptedTokens` explicitly — the middleware throws `X402ConfigError` on the first request to that route otherwise.
225
+ For any other network (from the full Parallel 24-chain catalog), you must provide `acceptedTokens` explicitly, or the middleware throws `X402ConfigError` on the first request to that route. The facilitator advertises routes on the four networks above only, and the invoice of any other network names it `eip155:<slug>`, which `@parallel-protocol/x402-fetch` cannot pay.
225
226
 
226
227
  You can also import the token list directly:
227
228
 
@@ -235,7 +236,7 @@ const defaults = getDefaultAcceptedTokens("base");
235
236
  // ["0x76A9...", "0x8335...", "0x472e..."] (USDp + USDC + sUSDp)
236
237
  ```
237
238
 
238
- **sUSDp** is included in the defaults (routes H, I, J, K, L are enabled out of the box). To opt out and accept only USDp and USDC, pass `acceptedTokens` explicitly:
239
+ **sUSDp** is included in the defaults, so the merchant can be paid in sUSDp: routes C, G and J deliver it. Routes H, I, K and L, where the agent pays with sUSDp, deliver USDp, USDC or a collateral and do not need it. To opt out and accept only USDp and USDC, pass `acceptedTokens` explicitly:
239
240
 
240
241
  ```typescript
241
242
  import { PARALLEL_TOKENS } from "@parallel-protocol/x402";
@@ -248,7 +249,7 @@ routes: {
248
249
  acceptedTokens: [
249
250
  PARALLEL_TOKENS.base.usdp,
250
251
  PARALLEL_TOKENS.base.usdc,
251
- // sUSDp omitted → routes H, I, J, K, L disabled for this route
252
+ // sUSDp omitted: routes C, G and J disabled for this route
252
253
  ],
253
254
  },
254
255
  }
@@ -279,14 +280,14 @@ routes: {
279
280
  acceptedTokens: [
280
281
  PARALLEL_TOKENS.ethereum.usdp,
281
282
  PARALLEL_TOKENS.ethereum.usdc,
282
- PARALLEL_TOKENS.ethereum.sUSDe, // opt-in → agents can pay with sUSDe
283
- PARALLEL_TOKENS.ethereum.sfrxUSD, // opt-in → agents can pay with sfrxUSD
283
+ PARALLEL_TOKENS.ethereum.sUSDe, // opt-in: the merchant can be paid in sUSDe
284
+ PARALLEL_TOKENS.ethereum.sfrxUSD, // opt-in: the merchant can be paid in sfrxUSD
284
285
  ],
285
286
  },
286
287
  }
287
288
  ```
288
289
 
289
- > Note: the facilitator validates that `tokenOut` is a whitelisted collateral on the target chain. If you add an address that is not in the Parallelizer's collateral list, payments to that route will be rejected at settlement.
290
+ > Note: the facilitator only delivers the chain's USDp, sUSDp and USDC and the collaterals in the table above. A payment that would deliver any other token is refused at verify, before the handler runs: a swap to such a token gets `COLLATERAL_NOT_SUPPORTED`. Listing such a token in `acceptedTokens` invites payments the facilitator refuses.
290
291
 
291
292
  ---
292
293
 
@@ -326,7 +327,7 @@ The agent attaches a `payment-signature` (or `X-PAYMENT`) header with a **base64
326
327
  // Minimal Route A payload (USDp transfer)
327
328
  const signedPayload = {
328
329
  scheme: "exact",
329
- network: "base",
330
+ network: "base", // the chain slug, not the eip155 form of the 402
330
331
  method: "transferWithAuthorization",
331
332
  payload: {
332
333
  signature: "0x...",
@@ -369,7 +370,7 @@ The `access-control-expose-headers` header is set automatically so browsers can
369
370
 
370
371
  ## Metered routes (`upto`)
371
372
 
372
- 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: `acceptedTokens` defaults to the network's USDp and may not list any other token.
373
+ 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: `acceptedTokens` defaults to the network's USDp and may not list any other token. They settle only on chains where the escrow is deployed, which today is Base alone (`UPTO_CHAINS` in `@parallel-protocol/chains`). On another chain the facilitator refuses the payment at verify with `PAYMENT_METHOD_NOT_SUPPORTED`.
373
374
 
374
375
  ```typescript
375
376
  routes: {
@@ -395,7 +396,7 @@ fastify.post("/v1/chat/completions", async (request, reply) => {
395
396
  });
396
397
  ```
397
398
 
398
- 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:
399
+ 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"`: digits with an optional fractional part, no sign, no exponent, no unit) and must not carry more decimals than the token has. Rules on a `2xx` response:
399
400
 
400
401
  | Header value | What happens |
401
402
  |---|---|
@@ -428,7 +429,7 @@ Instead of a header the handler sets, a route may carry a `meter` function that
428
429
 
429
430
  The response body is read only on routes that have a `meter` function, from a clone or from the buffered chunks the middleware already holds, so other routes are untouched. Reading it gets the handler's own time budget: a body that streams from a slow upstream and is not complete within the window is treated like a failing meter. Return a buffered body on metered routes. `meter` only applies to `upto` routes.
430
431
 
431
- Timing: the payer's authorization expires after `maxTimeoutSeconds`, and settlement may take up to two minutes, an attempt of up to 60 s and one retry. On Express, Hono and Next.js the handler timeout is raised to `maxTimeoutSeconds − 120 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.
432
+ Timing: the payer's authorization expires after `maxTimeoutSeconds`, and settlement may take up to two minutes, an attempt of up to 60 s and one retry. On Express, Hono and Next.js the handler timeout is raised to `maxTimeoutSeconds − 120 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 has done the work, but the payment can no longer settle: the client gets 402 `PAYMENT_SETTLEMENT_FAILED` with reason `PAYMENT_EXPIRED`, and the merchant is not paid. Keep a margin.
432
433
 
433
434
  ---
434
435
 
@@ -461,7 +462,7 @@ The function gets `{ method, path, url, header(name), body }`. `body` is the req
461
462
 
462
463
  The function runs twice per paid request: on the unpaid request, to build the 402, and on the paid retry, to size what the facilitator verifies. It must therefore be **pure**: same request, same price, no clock and no randomness. A retry whose body changed in between is priced again, and refused by the facilitator if the signed amount no longer covers the new price. It may be `async`.
463
464
 
464
- A function that throws or returns something that is not a price is a merchant bug: the request fails with `X402ConfigError` rather than being served for free or mispriced. To refuse a request the route will not serve (an unsupported option, a resource that does not exist) throw `X402PriceRejectedError(code, message, status = 400)` instead: the client gets that status with `{ error: code, message }`, and nothing is invoiced or signed.
465
+ A function that throws or returns something that is not a price is a merchant bug: the request fails with `X402ConfigError` rather than being served for free or mispriced. To refuse a request the route will not serve (an unsupported option, a resource that does not exist) throw `X402PriceRejectedError(code, message, status = 400)` instead: the client gets that status with `{ error: code, message }`, and nothing is invoiced or signed. A status outside 400 to 599 is sent as 400.
465
466
 
466
467
  ---
467
468
 
@@ -507,13 +508,13 @@ app.post("/v1/chat/completions", async (request, reply) => {
507
508
  });
508
509
  ```
509
510
 
510
- The same handler on the other frameworks: Express `res.status(upstream.status).json(upstream.body)`, Hono `c.json(upstream.body, upstream.status)`, Next.js `NextResponse.json(upstream.body, { status: upstream.status })`.
511
+ The same handler on the other frameworks: Express `res.status(upstream.status).json(upstream.body)`, Hono `c.json(upstream.body, upstream.status as ContentfulStatusCode)` (the type comes from `hono/utils/http-status`), Next.js `NextResponse.json(upstream.body, { status: upstream.status })`.
511
512
 
512
- `forwardChatCompletion(body, { baseUrl, apiKey, maxOutputTokens, outputTokensField, timeout, headers, fetch, fields })` posts to `${baseUrl}/chat/completions` with only the fields the cap accounts for (`FORWARDED_FIELDS`: model, messages, tools, sampling parameters, reasoning options), `stream: false` and the completion bounded by `maxOutputTokens` (sent as `max_tokens`, or `max_completion_tokens` for providers that want it; a `reasoning.max_tokens` budget is bounded the same way), and returns the provider's `{ status, body }`: a 2xx and the statuses about the client's request (400, 404, 413, 422, 429 and the like) are handed back as they are, so nothing is settled on an error; a status about the merchant's account with the provider (401, 402, 403, 5xx) is folded into 502 `UPSTREAM_ERROR` with a fixed body, so the client never sees the merchant's provider details; a provider that cannot be reached answers 502 `UPSTREAM_UNAVAILABLE`, a timeout 504 `UPSTREAM_TIMEOUT` (five minutes by default).
513
+ `forwardChatCompletion(body, { baseUrl, apiKey, maxOutputTokens, outputTokensField, timeout, headers, fetch, fields })` posts to `${baseUrl}/chat/completions` with only the fields the cap accounts for (`FORWARDED_FIELDS`: model, messages, tools and tool choice, response format, sampling parameters and penalties, stop sequences, seed, reasoning options, parallel tool calls, logit bias and `user`), `stream: false`, and the completion bounded by `maxOutputTokens`. The bound is sent as `max_tokens`, or as `max_completion_tokens` for providers that want it, and a `reasoning.max_tokens` budget is bounded the same way. It returns the provider's `{ status, body }`. A 2xx and the statuses about the client's request (400, 404, 405, 408, 409, 413, 415, 422 and 429) are handed back as they are, so nothing is settled on an error. Any other status, such as a 401, 402, 403 or 5xx about the merchant's account with the provider, is folded into 502 `UPSTREAM_ERROR` with a fixed body, so the client never sees the merchant's provider details. A provider that cannot be reached answers 502 `UPSTREAM_UNAVAILABLE`, and a timeout 504 `UPSTREAM_TIMEOUT` (five minutes by default).
513
514
 
514
515
  `chatCompletionMeter({ prices? })` reads the amount from the response: `usage.cost` in dollars when the provider reports one (OpenRouter does), otherwise `usage.prompt_tokens` and `usage.completion_tokens` at the model's prices when `prices` is given, otherwise nothing, which charges the cap. So a provider that reports no cost (DeepSeek, most direct APIs) needs the same `prices` as the cap, or every request settles at the cap with nothing refunded and no error to show it. A zero cost serves the request for free. Cached input tokens are charged at the full input price in the token-count path.
515
516
 
516
- The cap of a request is a bound on its input tokens at the model's input price, plus the largest completion the request may produce (`max_tokens`, bounded by `maxOutputTokens`, which also stands in for it when absent) at its output price. One USDp per dollar. The cap must cover the real count whichever tokenizer the provider uses, or the response is refused and the merchant has paid the provider for nothing, so the built-in bound counts one token per UTF-8 byte of the messages and tools, which no tokenizer exceeds. English prose runs at about four characters per token, so the cap is loose there: the payer only pays the provider's real count and the rest is refunded. A merchant who wants a tighter cap plugs a real tokenizer through `estimateInputTokens`.
517
+ The cap of a request is a bound on its input tokens at the model's input price, plus the largest completion the request may produce (`max_tokens` or `max_completion_tokens`, bounded by `maxOutputTokens`, which also stands in for it when absent) at its output price. One USDp per dollar. The cap must cover the real count whichever tokenizer the provider uses, or the response is refused and the merchant has paid the provider for nothing, so the built-in bound counts one token per UTF-8 byte of the messages and tools, which no tokenizer exceeds. English prose runs at about four characters per token, so the cap is loose there: the payer only pays the provider's real count and the rest is refunded. A merchant who wants a tighter cap plugs a real tokenizer through `estimateInputTokens`.
517
518
 
518
519
  | Option | Default | Description |
519
520
  |--------|---------|-------------|
@@ -528,7 +529,7 @@ A handler that does not use `forwardChatCompletion` should still forward only th
528
529
 
529
530
  `openRouterPrices({ ttl: "24h", timeout: "10s" })` loads the public models listing (no API key) on first use, serves it from memory and refreshes it in the background once older than `ttl`, so no request waits for OpenRouter after the first one; a load is abandoned after `timeout`, a refresh that fails keeps the current list, and a failed load is not retried for five seconds. With no list at all, requests are refused with a 503 `PRICING_UNAVAILABLE`. A model the list does not know may have been released since the list was loaded: once the list is more than five minutes old, such a request refreshes it in the background and is refused with an `UNKNOWN_MODEL` message that says so, and the next request finds the model. A fixed table never touches the network: `prices: { "deepseek-flash": { input: 0.30, output: 1.20 } }`.
530
531
 
531
- Requests the route cannot meter are refused before anything is invoiced, with a 400 and a JSON body: an unknown model (`UNKNOWN_MODEL`), a model outside `models` (`MODEL_NOT_ALLOWED`), more than one choice (`n`), non-text content, plugins, web search, image or audio output, or streaming (`UNSUPPORTED_OPTION`), a malformed body (`INVALID_REQUEST`). Live prices are the one accepted exception to the purity rule of price functions: a list refreshed between the unpaid request and the paid retry may re-price the retry, which is then refused and re-invoiced.
532
+ Requests the route cannot meter are refused before anything is invoiced, with a 400 and a JSON body: an unknown model (`UNKNOWN_MODEL`), a model outside `models` (`MODEL_NOT_ALLOWED`), more than one choice (`n`), non-text content, plugins, web search, image or audio output, or streaming (`UNSUPPORTED_OPTION`), a malformed body (`INVALID_REQUEST`). Live prices are the one accepted exception to the purity rule of price functions: a list refreshed between the unpaid request and the paid retry may re-price the retry. A retry priced above the amount the payer signed is refused and re-invoiced.
532
533
 
533
534
  Without `meter`, what is charged after the response is the handler's call: set the `payment-settle-amount` header to the provider's cost, see [Metered routes](#metered-routes-upto).
534
535
 
@@ -542,11 +543,11 @@ The facilitator supports 13 settlement routes. The route is selected automatical
542
543
  |-------|--------------------------|---------------------|------------------------------|----------|-------|
543
544
  | **A** | `transferWithAuthorization` | USDp | USDp | ~65K | Direct USDp transfer |
544
545
  | **B** | `swapExactOutputWithAuthorization` | USDp | USDC (exact) | ~120K | Parallelizer swaps agent's USDp → exact USDC for merchant |
545
- | **C** | `depositWithAuthorization` | USDp | sUSDp | ~90K | Agent's USDp deposited into savings vault; merchant receives sUSDp shares |
546
+ | **C** | `depositWithAuthorization` | USDp | sUSDp | ~110K | Agent's USDp deposited into the savings vault, the merchant receives sUSDp shares |
546
547
  | **D** | `swapExactOutputWithAuthorization` | USDp | non-USDC backing collateral (exact) | ~120K | Same as B but for other Parallelizer collaterals |
547
- | **E** | `swapExactInputWithAuthorization` | USDC (exact) | USDp (min) | ~120K | Agent pays exact USDC; Parallelizer delivers minimum USDp to merchant |
548
- | **F** | `transferWithAuthorization` | USDC (or any EIP-3009 token) | same token | ~65K | Direct EIP-3009 token push |
549
- | **G** | `swapExactInputWithAuthorization` | USDC (exact) | sUSDp | ~200K | USDC → USDp swap, then USDp deposited as sUSDp for merchant (atomic multicall3) |
548
+ | **E** | `swapExactInputWithAuthorization` | USDC or a collateral (exact) | USDp (min) | ~120K | Agent pays an exact amount, the Parallelizer delivers at least the minimum USDp to the merchant |
549
+ | **F** | `transferWithAuthorization` | USDC or a collateral | same token | ~65K | Direct EIP-3009 token push |
550
+ | **G** | `swapExactInputWithAuthorization` | USDC or a collateral (exact) | sUSDp | ~200K | The token is swapped to USDp, then the USDp is deposited as sUSDp for the merchant (atomic multicall3) |
550
551
  | **H** | `redeemWithAuthorization` | sUSDp shares | USDp | ~130K | Agent burns sUSDp shares; merchant receives USDp assets |
551
552
  | **I** | `redeemWithAuthorization` + `swapExactOutputWithAuthorization` | sUSDp | USDC (exact) | ~180K | Agent redeems sUSDp → USDp, then swaps USDp → exact USDC for merchant (atomic multicall3) |
552
553
  | **J** | `transferWithAuthorization` | sUSDp | sUSDp | ~65K | Direct sUSDp share transfer |
@@ -556,9 +557,12 @@ The facilitator supports 13 settlement routes. The route is selected automatical
556
557
 
557
558
  **Chain requirements:**
558
559
  - Routes B, D, E, G, I, K, L require the **Parallelizer** to be deployed on the target chain (Ethereum, Base, Avalanche, HyperEVM).
560
+ - Routes B and I swap to USDC, so they need USDC among the chain's collaterals, which is the case on Base and Avalanche.
559
561
  - Routes C, G, H, I, J, K, L require **sUSDp** (savings module) to be deployed on the target chain.
560
- - Route A and F work on all chains where the respective token is deployed.
562
+ - Route A needs USDp on the chain. Routes E, F and G take as `tokenIn` the chain's USDC or one of its collaterals (see [Default tokens](#default-tokens)), and the token must support EIP-3009. Any other `tokenIn` is refused at verify, with `PAYMENT_METHOD_NOT_SUPPORTED` when it gets past the signature checks.
563
+ - Route U needs the escrow contract, deployed on Base only. A metered payment on another chain is refused at verify with `PAYMENT_METHOD_NOT_SUPPORTED`.
561
564
  - Gas sponsorship is decided by the facilitator per payment and reported as `gasSponsored` in the settlement receipt.
565
+ - The facilitator's `GET /capabilities?chain=<slug>` lists the routes available on a chain right now, with the gas estimates above and the burn fees.
562
566
 
563
567
  ---
564
568
 
@@ -573,7 +577,14 @@ import {
573
577
  createPaymentMiddleware,
574
578
  resolveCharge,
575
579
  resolveSettleAmount,
580
+ settleVerified,
581
+ settlementFailureResponse,
576
582
  SETTLE_AMOUNT_HEADER,
583
+ type ChargeResponse,
584
+ type SettleAmountResolution,
585
+ type SettlementFailure,
586
+ type SettlementFailureBody,
587
+ type SettlementOutcome,
577
588
 
578
589
  // Facilitator client
579
590
  FacilitatorClient,
@@ -593,7 +604,9 @@ import {
593
604
 
594
605
  // Types
595
606
  type FacilitatorConfig,
607
+ type FacilitatorPaymentRequirements,
596
608
  type FacilitatorResponse,
609
+ type FacilitatorSettleOptions,
597
610
  type HTTPAdapter,
598
611
  type MeterContext,
599
612
  type MeterFunction,
@@ -609,7 +622,11 @@ import {
609
622
  type PriceFunction,
610
623
  type ResourceInfo,
611
624
  type RouteConfig,
625
+ type RunHandlerOptions,
612
626
  type RunHandlerResult,
627
+ type SettleOptions,
628
+ type VerifiedPayment,
629
+ type VerifyResult,
613
630
 
614
631
  // Utils
615
632
  encodeBase64,
@@ -649,9 +666,11 @@ import {
649
666
  type ChatCompletionCapOptions,
650
667
  type ChatCompletionMeterOptions,
651
668
  type ChatCompletionBody,
669
+ type ChatMessage,
652
670
  type ForwardChatCompletionOptions,
653
671
  type ForwardedResponse,
654
672
  type ModelPrice,
673
+ type OpenRouterPricesOptions,
655
674
  type PriceSource,
656
675
  type PriceTable,
657
676
  } from "@parallel-protocol/x402/llm";
@@ -674,19 +693,21 @@ const result = await gate({
674
693
  getMethod: () => request.method,
675
694
  getPath: () => request.path,
676
695
  getUrl: () => request.url,
696
+ getBody: async () => request.body, // optional: the parsed body, for price and meter functions
677
697
  });
678
698
 
679
699
  if (result.type === "pass") {
680
700
  // Route not configured for payment, proceed normally
681
701
  } else if (result.type === "error") {
682
- // Send 402: result.result.status / result.result.headers / result.result.body
702
+ // Send result.result.status (402, or the status of a price refusal)
703
+ // with result.result.headers and result.result.body
683
704
  } else {
684
705
  // result.type === "verified"
685
706
  // Run your handler...
686
707
  const handlerStatus = await runHandler();
687
708
 
688
709
  if (handlerStatus >= 200 && handlerStatus < 300) {
689
- const settlement = await result.settle();
710
+ const settlement = await result.settle(); // a metered route passes { amount }
690
711
  if (settlement.success) {
691
712
  // settlement.txHash / settlement.route / settlement.gasSponsored
692
713
  }
@@ -694,7 +715,7 @@ if (result.type === "pass") {
694
715
  }
695
716
  ```
696
717
 
697
- `createPaymentGate` only verifies; it does not run the handler. Use `createPaymentMiddleware` if you want the full verify → run → settle flow managed automatically. The gate keeps the settle token the facilitator issued at verify and presents it in `result.settle()`, so a custom integration has nothing to carry. `settleVerified(result, options)` runs the same settlement the adapters use (one retry when the outcome is unknown, the settled amount checked on metered routes) and `settlementFailureResponse(outcome)` builds the 402 for a failed one.
718
+ `createPaymentGate` only verifies, it does not run the handler. Use `createPaymentMiddleware` if you want the whole flow (verify, run the handler, settle) managed automatically. The gate keeps the settle token the facilitator issued at verify and presents it in `result.settle()`, so a custom integration has nothing to carry. On a metered route, `result.settle({ amount })` takes the amount to charge in the token's smallest unit, which `resolveCharge` computes from the handler's response as the adapters do. `result.settle()` makes a single call and throws as [`FacilitatorClient.settle`](#facilitatorclient) does. `settleVerified(result, options)` runs the same settlement the adapters use (one retry when the outcome is unknown, the settled amount checked on metered routes) and `settlementFailureResponse(outcome)` builds the 402 for a failed one.
698
719
 
699
720
  ---
700
721
 
@@ -734,19 +755,31 @@ if (result.success) {
734
755
  }
735
756
  ```
736
757
 
737
- `verify` uses a 10-second timeout. `settle` uses a 60-second timeout to accommodate on-chain confirmation latency. `verify` throws on any failure, with the facilitator's message in `.detail` and its HTTP status in `.status`; a facilitator that issues no settle token is an invalid response. `settle` returns a `FacilitatorResponse` union on facilitator errors and throws `X402RuntimeError` on connectivity issues (`SETTLE_TIMEOUT` when the 60 seconds ran out, the payment may still settle), an undecodable payment header, or a malformed facilitator response. The settle token is a secret for the life of the request: keep it in memory, never log it, never send it to the payer.
758
+ The constructor throws `X402ConfigError` when the URL is neither https nor plain http on `localhost`, `127.0.0.1` or `[::1]`, and the client keeps the address it checked. No call follows a redirect. `verify` uses a 10-second timeout. `settle` uses a 60-second timeout to accommodate on-chain confirmation latency. `verify` throws on any failure, with the facilitator's message in `.detail`, its HTTP status in `.status` and its `Retry-After` in `.retryAfter`. A facilitator that issues no settle token is an invalid response. `settle` returns a `FacilitatorResponse` union on facilitator errors, with the facilitator's status and `Retry-After` on `error`, and throws `X402RuntimeError` on connectivity issues (`SETTLE_TIMEOUT` when the 60 seconds ran out, the payment may still settle), an undecodable payment header, or a malformed facilitator response. A settle refused with a status below 500 and no `error` field comes back as `SUBMISSION_FAILED`, one at 500 or above with no `error` field throws `FACILITATOR_UNAVAILABLE`. The settle token is a secret for the life of the request: keep it in memory, never log it, never send it to the payer.
738
759
 
739
- A single-call `client.pay(paymentHeader, requirements)` (`POST /x402/pay`) also exists — it verifies and settles in one round trip, without the two-phase guarantees the middleware relies on.
760
+ For a metered payment, the requirements also carry `scheme: "upto"` and `maxTimeoutSeconds`, and `settle` takes the amount to charge, in the token's smallest unit, next to the settle token: `{ settleToken, amount }`.
740
761
 
741
762
  ---
742
763
 
743
764
  ## Error codes
744
765
 
766
+ ### Responses to the agent
767
+
768
+ What the middleware answers on a paid route instead of the handler's response:
769
+
770
+ | Status | Body | When |
771
+ |--------|------|------|
772
+ | 402 | `PaymentRequired`, with `error` and `message` when a payment was refused | No payment header, or the payment was refused at verify, see [Propagated from the facilitator](#propagated-from-the-facilitator) |
773
+ | 402 | `{ error: "PAYMENT_SETTLEMENT_FAILED", reason, message }` | The handler answered 2xx and the payment could not be settled. The handler's response is discarded |
774
+ | 402 | `{ error: "INVALID_SETTLE_AMOUNT", message }` | A metered route named an amount the middleware cannot settle, see [Metered routes](#metered-routes-upto) |
775
+ | 504 | `{ error: "HANDLER_TIMEOUT", message }` on Express and Hono, an empty body on Next.js | The handler did not answer within its timeout, and nothing is settled. Next.js also answers this empty 504 when the handler throws |
776
+ | 400, or the status the price function chose | `{ error, message }` | A price function refused the request with `X402PriceRejectedError`, and nothing was invoiced |
777
+
745
778
  ### Thrown by the SDK
746
779
 
747
780
  | Error class | When |
748
781
  |-------------|------|
749
- | `X402ConfigError` | Invalid config at construction (bad `payTo`, bad facilitator URL, empty `routes`, non-positive `price`) — plus, on the first request to a route, an unknown network with no `acceptedTokens`, or an accepted token whose decimals are neither in the catalog nor set via `decimals` |
782
+ | `X402ConfigError` | Invalid config at construction, the message naming the first field at fault: a bad `payTo`, a facilitator URL that is neither https nor plain http on `localhost`, `127.0.0.1` or `[::1]`, empty `routes`, a malformed or non-positive `price`, a `meter` on a route that is not `upto`, or an `upto` route with `maxTimeoutSeconds` above 3600 or a token other than USDp. Then, on a request to a route: an unknown network with no `acceptedTokens`, an accepted token whose decimals are neither in the catalog nor set via `decimals`, a price finer than the token's precision, or a price function that throws or returns something that is not a price |
750
783
  | `X402RuntimeError` | Runtime failure; `.code` is one of the SDK codes below, or a facilitator error code propagated verbatim (see next table) |
751
784
  | `X402PriceRejectedError` | Thrown by a price function to refuse a request before anything is invoiced; the client gets `.status` (400 by default) with `{ error: .code, message }`. `isPriceRejection(err)` recognises one by shape. |
752
785
 
@@ -762,48 +795,81 @@ Refusals `chatCompletionCap` makes, all before payment:
762
795
 
763
796
  | `X402RuntimeError.code` | Meaning |
764
797
  |-------------------------|---------|
765
- | `FACILITATOR_UNAVAILABLE` | Facilitator timed out on verify (10s) or was unreachable |
798
+ | `FACILITATOR_UNAVAILABLE` | The facilitator gave no usable answer: unreachable, too slow on verify, a redirect, an error without an `error` field, or a body that is not JSON (see below) |
766
799
  | `SETTLE_TIMEOUT` | Facilitator did not answer a settle within 60s. The payment may still settle |
767
- | `FACILITATOR_INVALID_RESPONSE` | Facilitator returned an unexpected response shape, including a verify without a settle token |
768
- | `INVALID_PAYMENT` | `payment-signature` / `X-PAYMENT` header could not be base64-decoded |
800
+ | `FACILITATOR_INVALID_RESPONSE` | Facilitator returned an unexpected response shape, including a verify without a settle token. The middleware also gives this code to a verify refusal whose `error` is not a code, see [Propagated from the facilitator](#propagated-from-the-facilitator) |
801
+ | `INVALID_PAYMENT` | `payment-signature` / `X-PAYMENT` header is not base64-encoded JSON |
802
+
803
+ #### `FACILITATOR_UNAVAILABLE`
804
+
805
+ The SDK raises it when the facilitator gave no usable answer:
806
+
807
+ - It could not be reached: a DNS, connection or TLS failure.
808
+ - It answered with a redirect, which the SDK never follows. The error's `status` is the redirect's, and the configured `url` is then usually not the facilitator's own https address.
809
+ - It did not answer a `verify` within 10 seconds. A `settle` that runs out of its 60 seconds raises `SETTLE_TIMEOUT` instead.
810
+ - It answered a `verify` with an error that carries no `error` field, such as an error page from a gateway in front of it, or a `settle` with a 5xx error that carries no `error` field.
811
+ - It answered with a success status but a body that is not JSON, or that was cut off.
812
+
813
+ At `verify`, nothing was settled. The middleware answers the agent 402 with a new invoice and `error: "FACILITATOR_UNAVAILABLE"`, and the agent can pay again later.
814
+
815
+ At `settle`, the outcome is unknown, since the facilitator may have settled before its answer was lost. The middleware retries the settle once with the same settle token, which returns the recorded outcome if there is one. If the retry raises it too, the response is not served, and the agent gets a 402 `{ error: "PAYMENT_SETTLEMENT_FAILED", reason: "FACILITATOR_UNAVAILABLE" }` whose message tells it not to sign another payment before checking with the merchant. A retry that fails another way reports that reason instead.
769
816
 
770
817
  ### Propagated from the facilitator
771
818
 
772
- When the facilitator refuses a payment at verify, its code is forwarded as the `error` field on the re-issued 402 body and its text as `message`. Common values:
773
-
774
- | Code | Meaning |
775
- |------|---------|
776
- | `INVALID_PAYLOAD` | The payload does not have the shape of its route, or carries a field another route would read |
777
- | `INVALID_SIGNATURE` | EIP-712 signature recovery failed |
778
- | `INVALID_NONCE` | Nonce already used (replay detected) |
779
- | `PAYMENT_EXPIRED` | `validBefore` has passed |
780
- | `INSUFFICIENT_AMOUNT` | The amount the route guarantees the merchant is below the price |
781
- | `INSUFFICIENT_BALANCE` | The payer does not hold the signed amount (metered payments) |
782
- | `ASSET_MISMATCH` | The route does not deliver the token the merchant requires |
783
- | `RECIPIENT_MISMATCH` | The signed recipient is not the merchant's `payTo` |
784
- | `NETWORK_MISMATCH` | Payload chain ≠ route config chain |
785
- | `WINDOW_TOO_LONG` | A metered authorization valid for longer than the facilitator allows |
786
- | `PAYMENT_METHOD_NOT_SUPPORTED` | No route for this method or token on this chain |
787
- | `RATE_LIMIT_EXCEEDED` | Signer has exceeded the hourly transaction limit |
819
+ When the facilitator refuses a payment at verify, its code is forwarded as the `error` field on the re-issued 402 body, its text as `message`, and its `Retry-After` header is copied when it sent one. The agent always gets a 402. The facilitator's own status is the `.status` of the `X402RuntimeError` that `FacilitatorClient.verify` throws. The codes:
820
+
821
+ | Code | Facilitator status | Meaning | What the agent should do |
822
+ |------|--------------------|---------|--------------------------|
823
+ | `INVALID_PAYLOAD` | 400 | The payload does not have the shape of its route, carries a field another route would read, or uses a method of the other scheme | Sign a payment that matches one of the `accepts` entries |
824
+ | `INVALID_SIGNATURE` | 400 | EIP-712 signature recovery failed | Sign again |
825
+ | `INVALID_NONCE` | 409 | Nonce already used (replay detected) | Sign a new payment with a fresh nonce |
826
+ | `PAYMENT_EXPIRED` | 402 | `validBefore` has passed | Sign a new payment |
827
+ | `INSUFFICIENT_AMOUNT` | 402 | The amount the route guarantees the merchant is below the price, or the authorization is for zero | Sign for the invoiced `amount` |
828
+ | `INSUFFICIENT_BALANCE` | 402 | The payer does not hold the signed amount (metered payments) | Fund the wallet, sign a new payment |
829
+ | `ASSET_MISMATCH` | 400 | The route does not deliver the token the merchant requires | Pay so that the merchant receives one of the `accepts` assets |
830
+ | `RECIPIENT_MISMATCH` | 400 | The signed recipient is not the merchant's `payTo` | Sign to the invoiced `payTo` |
831
+ | `NETWORK_MISMATCH` | 400 | The facilitator does not support the payload's chain, or it is not the route's chain | Sign on the invoiced network |
832
+ | `WINDOW_TOO_LONG` | 400 | A metered authorization valid for more than 3600 s (one hour) | Sign again with a `validBefore` within the hour |
833
+ | `PAYMENT_METHOD_NOT_SUPPORTED` | 400 | No route for this method or token on this chain, including a metered payment on a chain without the escrow | Pay with another method or token |
834
+ | `COLLATERAL_NOT_SUPPORTED` | 400 | The swap delivers a token that is not a collateral of the chain | Pay for another of the `accepts` assets: the merchant lists one the facilitator does not deliver |
835
+ | `RATE_LIMIT_EXCEEDED` | 429 | The payer passed 1,000 verifies in the current UTC hour | Pay again after `Retry-After`, which points to the next hour |
836
+ | `SERVICE_UNAVAILABLE` | 503 | The facilitator could not read the chain or its own store | Pay again after a short wait |
837
+
838
+ An `error` that is not a code (capitals, digits and underscores) is not forwarded: the 402 carries `error: "FACILITATOR_INVALID_RESPONSE"` with the facilitator's message instead. The facilitator answers this way with `Bad Request` to a payment payload that does not match its request schema, and with `Too Many Requests` from its per-IP limit (see [Rate limits](#rate-limits)).
788
839
 
789
840
  When the facilitator refuses at settle, the 402 body is `{ error: "PAYMENT_SETTLEMENT_FAILED", reason, message }`, with the facilitator's `Retry-After` header copied when it sent one. `reason` values from the facilitator:
790
841
 
791
- | Reason | Meaning | What the agent should do |
792
- |--------|---------|--------------------------|
793
- | `INSUFFICIENT_BALANCE` | The payer no longer holds the signed amount | Fund the wallet, sign a new payment |
794
- | `AUTHORIZATION_USED` | The authorization nonce is consumed or cancelled on-chain, this facilitator did not settle it | Sign a new payment |
795
- | `PAYMENT_EXPIRED` | The authorization expired before settlement | Sign a new payment |
796
- | `PAYMENT_NOT_VERIFIED` | No verified payment for this authorization, or it already failed | Sign a new payment |
797
- | `SETTLE_IN_PROGRESS` | Another settle of the same payment is still running | Do not sign another payment before checking with the merchant |
798
- | `SETTLE_TOKEN_INVALID`, `SETTLE_TOKEN_REQUIRED` | The merchant did not present the settle token verify issued: a merchant client issue, not the payer's, and nothing was settled | Do not sign another payment before checking with the merchant |
799
- | `RELAYER_UNAVAILABLE`, `PARALLELIZER_PAUSED`, `SERVICE_UNAVAILABLE` | The facilitator or the chain is temporarily unable to settle | Retry later with a new payment, after `Retry-After` when given |
800
- | `TX_REVERTED`, `SUBMISSION_FAILED` | The chain refused for a reason the facilitator could not attribute | Sign a new payment |
801
- | `RECEIPT_UNAVAILABLE` | The transaction was sent but its confirmation could not be read | Do not sign another payment before checking with the merchant |
802
-
803
- Reasons from this SDK: `SETTLE_TIMEOUT`, `FACILITATOR_UNAVAILABLE` and `FACILITATOR_INVALID_RESPONSE` (the settle may still have gone through, do not pay again before checking with the merchant), `AMOUNT_MISMATCH` (the facilitator settled another amount than the one asked, money moved, the response was not served, do not pay again before checking), `UNKNOWN` (anything that is not a code). For `SETTLE_IN_PROGRESS`, `RECEIPT_UNAVAILABLE`, `SETTLE_TOKEN_INVALID` and `SETTLE_TOKEN_REQUIRED` the message is this SDK's as well, since the facilitator's text addresses the merchant.
842
+ | Reason | Facilitator status | Meaning | What the agent should do |
843
+ |--------|--------------------|---------|--------------------------|
844
+ | `INSUFFICIENT_BALANCE` | 402 | The payer no longer holds the signed amount | Fund the wallet, sign a new payment |
845
+ | `AUTHORIZATION_USED` | 409 | The authorization nonce is consumed or cancelled on-chain, this facilitator did not settle it | Sign a new payment |
846
+ | `PAYMENT_EXPIRED` | 402 | The authorization expired before settlement | Sign a new payment |
847
+ | `PAYMENT_NOT_VERIFIED` | 402 | No verified payment for this authorization, or it already failed | Sign a new payment |
848
+ | `SETTLE_IN_PROGRESS` | 409 | Another settle of the same payment is still running | Do not sign another payment before checking with the merchant |
849
+ | `SETTLE_TOKEN_INVALID`, `SETTLE_TOKEN_REQUIRED` | 403 | The merchant did not present the settle token verify issued: a merchant client issue, not the payer's, and nothing was settled | Do not sign another payment before checking with the merchant |
850
+ | `RELAYER_UNAVAILABLE`, `PARALLELIZER_PAUSED`, `SERVICE_UNAVAILABLE` | 503 | The facilitator or the chain is temporarily unable to settle | Retry later with a new payment, after `Retry-After` when given |
851
+ | `TX_REVERTED` | 500 | The transaction reverted for a reason the facilitator could not attribute | Sign a new payment |
852
+ | `SUBMISSION_FAILED` | 500 | The submission failed. When the message says the outcome is unknown, a transaction may have been sent, and the payment may have settled or may still settle | Sign a new payment, unless the message says the outcome is unknown: then check with the merchant first |
853
+ | `RECEIPT_UNAVAILABLE` | 500 | The transaction was sent but its confirmation could not be read | Do not sign another payment before checking with the merchant |
854
+ | `INVALID_PAYLOAD`, `NETWORK_MISMATCH`, `PAYMENT_METHOD_NOT_SUPPORTED` | 400 | The settle request does not match the payment that was verified, or a metered amount is missing, malformed or above the signed cap. Nothing was settled | Sign a new payment |
855
+ | `INSUFFICIENT_AMOUNT` | 402 | A metered amount of zero, or an authorization for zero. Nothing was settled | Sign a new payment |
856
+
857
+ Reasons from this SDK: `SETTLE_TIMEOUT`, `FACILITATOR_UNAVAILABLE` and `FACILITATOR_INVALID_RESPONSE` (the settle may still have gone through, do not pay again before checking with the merchant), `AMOUNT_MISMATCH` (the facilitator settled another amount than the one asked, money moved, the response was not served, do not pay again before checking), `UNKNOWN` (anything that is not a code, such as the `Too Many Requests` of the facilitator's per-IP limit). For `SETTLE_IN_PROGRESS`, `RECEIPT_UNAVAILABLE`, `SETTLE_TOKEN_INVALID` and `SETTLE_TOKEN_REQUIRED` the message is this SDK's as well, since the facilitator's text addresses the merchant.
804
858
 
805
859
  `AMOUNT_MISMATCH` is also the one reason that points at the facilitator rather than at the request: it confirmed a settlement of another amount than the one the route asked for, so money moved. Alert on it, for example on 402 responses whose `reason` is `AMOUNT_MISMATCH`, rather than treating it as a routine refusal.
806
860
 
861
+ ### Rate limits
862
+
863
+ The facilitator counts the requests of each IP address that calls it, which is the merchant's server: 60 `POST /x402/verify` a minute and 600 `POST /x402/settle` a minute, each on its own counter. It also counts the verifies of each payer: 1,000 per UTC clock hour.
864
+
865
+ | Limit | Facilitator answer | What the agent gets | What the agent should do |
866
+ |-------|--------------------|---------------------|--------------------------|
867
+ | Verifies per IP | 429 `{ error: "Too Many Requests", message }`, the message naming the time left in the minute (`Rate limit exceeded, retry in 42 seconds`, for instance), with `Retry-After` in seconds | 402 with a new invoice, `error: "FACILITATOR_INVALID_RESPONSE"`, the facilitator's message and its `Retry-After`. Nothing was verified | Send the payment again after `Retry-After` |
868
+ | Settles per IP | The same 429 | 402 `{ error: "PAYMENT_SETTLEMENT_FAILED", reason: "UNKNOWN", message: "The payment could not be settled." }` with the facilitator's `Retry-After`. The handler's response is discarded, the settle is not retried, and nothing was settled | The agent cannot tell this `UNKNOWN` from another one, and x402-fetch reports that the payment may still settle: check with the merchant, then sign a new payment after `Retry-After` |
869
+ | Verifies per payer | 429 `RATE_LIMIT_EXCEEDED`, with `Retry-After` until the next UTC hour | 402 with a new invoice, `error: "RATE_LIMIT_EXCEEDED"`, the facilitator's message and its `Retry-After` | Pay again after `Retry-After` |
870
+
871
+ The `error` of the per-IP answer is not a code, which is why the middleware reports `FACILITATOR_INVALID_RESPONSE` at verify and `UNKNOWN` at settle. A direct caller of `FacilitatorClient` sees it as it is: `verify` throws an `X402RuntimeError` whose `.code` is `"Too Many Requests"`, with `.status` 429 and `.retryAfter`, and `settle` returns it as a failure with the same code.
872
+
807
873
  ---
808
874
 
809
875
  ## Utilities
@@ -841,9 +907,9 @@ import {
841
907
  | Decimals | per accepted token (chains catalog) | 6 (fixed default) |
842
908
  | Auth standard | x402 / Coinbase | RFC 9110 `Payment` auth-scheme |
843
909
  | Facilitator endpoints | `/x402/verify`, `/x402/settle` | `/mpp/verify`, `/mpp/settle` |
844
- | On-chain routes | A–L | A–L (same) |
910
+ | On-chain routes | A to L, and U for `upto` | A to L, except G |
845
911
 
846
- Both protocols share the same facilitator backend, the same `SignedPaymentPayload` authorization format, and the same settlement routes.
912
+ Both protocols settle through the same facilitator backend. MPP reaches routes A to L except G, its `parallel` method carrying the same `SignedPaymentPayload` as x402. Route U, for metered payments, is x402 only.
847
913
 
848
914
  ---
849
915
 
@@ -851,10 +917,11 @@ Both protocols share the same facilitator backend, the same `SignedPaymentPayloa
851
917
 
852
918
  | Package | Purpose |
853
919
  |---------|---------|
920
+ | [`@parallel-protocol/x402-fetch`](../x402-fetch) | The payer side: a `fetch` that pays these 402 challenges |
854
921
  | [`@parallel-protocol/payment-core`](../payment-core) | EIP-3009 authorization builders and EIP-712 signing helpers (agent-side) |
855
922
  | [`@parallel-protocol/x402-types`](../x402-types) | The facilitator's HTTP wire contract (Zod schemas) |
856
923
  | [`@parallel-protocol/mpp`](../mpp) | Machine Payments Protocol middleware (IETF `Payment` auth-scheme) |
857
- | [`@parallel-protocol/chains`](../chains) | 25-chain catalog with contract addresses |
924
+ | [`@parallel-protocol/chains`](../chains) | 24-chain catalog with contract addresses |
858
925
 
859
926
  ---
860
927
 
@@ -1,4 +1,4 @@
1
- import { X402_ERROR_CODES, X402RuntimeError, X402ConfigError, isPriceRejection } from './chunk-QAECRSWF.js';
1
+ import { X402_ERROR_CODES, X402ConfigError, X402RuntimeError, isPriceRejection } from './chunk-QAECRSWF.js';
2
2
  import { getTokenDecimals as getTokenDecimals$1 } from '@parallel-protocol/chains';
3
3
  import { z } from 'zod';
4
4
 
@@ -127,8 +127,18 @@ var priceSchema = z.union([
127
127
  "Price must be a string, a bigint or a function of the request"
128
128
  )
129
129
  ]);
130
+ var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
131
+ function isFacilitatorUrl(value) {
132
+ if (!URL.canParse(value)) return false;
133
+ const { protocol, hostname } = new URL(value);
134
+ return protocol === "https:" || protocol === "http:" && LOCAL_HOSTNAMES.has(hostname);
135
+ }
136
+ var facilitatorUrlSchema = z.string().refine(
137
+ isFacilitatorUrl,
138
+ "The facilitator URL must use https (plain http only on localhost, 127.0.0.1 or [::1])"
139
+ );
130
140
  var facilitatorConfigSchema = z.object({
131
- url: z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
141
+ url: facilitatorUrlSchema,
132
142
  apiKey: z.string().optional()
133
143
  });
134
144
  var UPTO_MAX_TIMEOUT_SECONDS = 3600;
@@ -223,6 +233,15 @@ function retryAfterOf(response) {
223
233
  const value = response.headers.get("retry-after");
224
234
  return value !== null && RETRY_AFTER_SHAPE.test(value) ? value : void 0;
225
235
  }
236
+ function throwIfRedirect(response) {
237
+ const redirected = response.type === "opaqueredirect" || response.status >= 300 && response.status < 400;
238
+ if (!redirected) return;
239
+ response.body?.cancel().catch(() => {
240
+ });
241
+ throw new X402RuntimeError(X402_ERROR_CODES.FACILITATOR_UNAVAILABLE, {
242
+ status: response.status
243
+ });
244
+ }
226
245
  function decodePaymentHeader(paymentHeader) {
227
246
  try {
228
247
  return decodeBase64(paymentHeader);
@@ -231,20 +250,33 @@ function decodePaymentHeader(paymentHeader) {
231
250
  }
232
251
  }
233
252
  var FacilitatorClient = class {
253
+ // Checked and kept when the client is built: a config changed afterwards
254
+ // does not move the calls elsewhere.
255
+ url;
256
+ apiKey;
234
257
  constructor(config) {
235
- this.config = config;
258
+ const url = facilitatorUrlSchema.safeParse(config.url);
259
+ if (!url.success) {
260
+ throw new X402ConfigError(
261
+ url.error.issues[0]?.message ?? "Invalid facilitator URL",
262
+ url.error
263
+ );
264
+ }
265
+ this.url = new URL(url.data).href.replace(/\/+$/, "");
266
+ this.apiKey = config.apiKey;
236
267
  }
237
- config;
238
268
  async verify(paymentHeader, paymentRequirements) {
239
269
  const paymentPayload = decodePaymentHeader(paymentHeader);
240
270
  let json;
241
271
  try {
242
- const response = await fetch(`${this.config.url}/x402/verify`, {
272
+ const response = await fetch(`${this.url}/x402/verify`, {
243
273
  method: "POST",
244
274
  headers: this.buildHeaders(),
245
275
  body: JSON.stringify({ paymentPayload, paymentRequirements }),
276
+ redirect: "manual",
246
277
  signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
247
278
  });
279
+ throwIfRedirect(response);
248
280
  if (!response.ok) {
249
281
  const body = await readErrorBody(response);
250
282
  throw new X402RuntimeError(
@@ -272,7 +304,7 @@ var FacilitatorClient = class {
272
304
  const paymentPayload = decodePaymentHeader(paymentHeader);
273
305
  let json;
274
306
  try {
275
- const response = await fetch(`${this.config.url}/x402/settle`, {
307
+ const response = await fetch(`${this.url}/x402/settle`, {
276
308
  method: "POST",
277
309
  headers: this.buildHeaders(),
278
310
  // settleAmount only travels for metered routes; the settle token
@@ -287,8 +319,10 @@ var FacilitatorClient = class {
287
319
  settleToken: options.settleToken
288
320
  }
289
321
  }),
322
+ redirect: "manual",
290
323
  signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
291
324
  });
325
+ throwIfRedirect(response);
292
326
  if (!response.ok) {
293
327
  const body = await readErrorBody(response);
294
328
  if (body.error === void 0 && response.status >= 500) {
@@ -337,16 +371,22 @@ var FacilitatorClient = class {
337
371
  throw new X402RuntimeError(X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
338
372
  }
339
373
  }
374
+ /**
375
+ * @deprecated The Parallel facilitator no longer serves `/x402/pay`, so this
376
+ * call fails with `FACILITATOR_UNAVAILABLE`. Use `verify`, then `settle`.
377
+ */
340
378
  async pay(paymentHeader) {
341
379
  const headers = this.buildHeaders();
342
380
  headers["X-PAYMENT"] = paymentHeader;
343
381
  let json;
344
382
  try {
345
- const response = await fetch(`${this.config.url}/x402/pay`, {
383
+ const response = await fetch(`${this.url}/x402/pay`, {
346
384
  method: "POST",
347
385
  headers,
386
+ redirect: "manual",
348
387
  signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
349
388
  });
389
+ throwIfRedirect(response);
350
390
  if (!response.ok) {
351
391
  throw new X402RuntimeError(X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
352
392
  }
@@ -365,8 +405,8 @@ var FacilitatorClient = class {
365
405
  const headers = {
366
406
  "Content-Type": "application/json"
367
407
  };
368
- if (this.config.apiKey) {
369
- headers["X-API-Key"] = this.config.apiKey;
408
+ if (this.apiKey) {
409
+ headers["X-API-Key"] = this.apiKey;
370
410
  }
371
411
  return headers;
372
412
  }
@@ -650,12 +690,13 @@ function paymentRequiredResponse(url, config, resolvedTokens, price, refusal) {
650
690
  });
651
691
  }
652
692
  function createPaymentGate(config) {
653
- try {
654
- paymentMiddlewareConfigSchema.parse(config);
655
- } catch (cause) {
693
+ const checked = paymentMiddlewareConfigSchema.safeParse(config);
694
+ if (!checked.success) {
695
+ const issue = checked.error.issues[0];
696
+ const where = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
656
697
  throw new X402ConfigError(
657
- "Invalid paymentMiddleware config \u2014 check your routes, facilitator URL and payTo addresses",
658
- cause
698
+ `Invalid paymentMiddleware config${where}: ${issue?.message ?? "check your routes, facilitator URL and payTo addresses"}`,
699
+ checked.error
659
700
  );
660
701
  }
661
702
  const client = new FacilitatorClient(config.facilitator);
@@ -129,8 +129,18 @@ var priceSchema = zod.z.union([
129
129
  "Price must be a string, a bigint or a function of the request"
130
130
  )
131
131
  ]);
132
+ var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
133
+ function isFacilitatorUrl(value) {
134
+ if (!URL.canParse(value)) return false;
135
+ const { protocol, hostname } = new URL(value);
136
+ return protocol === "https:" || protocol === "http:" && LOCAL_HOSTNAMES.has(hostname);
137
+ }
138
+ var facilitatorUrlSchema = zod.z.string().refine(
139
+ isFacilitatorUrl,
140
+ "The facilitator URL must use https (plain http only on localhost, 127.0.0.1 or [::1])"
141
+ );
132
142
  var facilitatorConfigSchema = zod.z.object({
133
- url: zod.z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
143
+ url: facilitatorUrlSchema,
134
144
  apiKey: zod.z.string().optional()
135
145
  });
136
146
  var UPTO_MAX_TIMEOUT_SECONDS = 3600;
@@ -225,6 +235,15 @@ function retryAfterOf(response) {
225
235
  const value = response.headers.get("retry-after");
226
236
  return value !== null && RETRY_AFTER_SHAPE.test(value) ? value : void 0;
227
237
  }
238
+ function throwIfRedirect(response) {
239
+ const redirected = response.type === "opaqueredirect" || response.status >= 300 && response.status < 400;
240
+ if (!redirected) return;
241
+ response.body?.cancel().catch(() => {
242
+ });
243
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE, {
244
+ status: response.status
245
+ });
246
+ }
228
247
  function decodePaymentHeader(paymentHeader) {
229
248
  try {
230
249
  return decodeBase64(paymentHeader);
@@ -233,20 +252,33 @@ function decodePaymentHeader(paymentHeader) {
233
252
  }
234
253
  }
235
254
  var FacilitatorClient = class {
255
+ // Checked and kept when the client is built: a config changed afterwards
256
+ // does not move the calls elsewhere.
257
+ url;
258
+ apiKey;
236
259
  constructor(config) {
237
- this.config = config;
260
+ const url = facilitatorUrlSchema.safeParse(config.url);
261
+ if (!url.success) {
262
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
263
+ url.error.issues[0]?.message ?? "Invalid facilitator URL",
264
+ url.error
265
+ );
266
+ }
267
+ this.url = new URL(url.data).href.replace(/\/+$/, "");
268
+ this.apiKey = config.apiKey;
238
269
  }
239
- config;
240
270
  async verify(paymentHeader, paymentRequirements) {
241
271
  const paymentPayload = decodePaymentHeader(paymentHeader);
242
272
  let json;
243
273
  try {
244
- const response = await fetch(`${this.config.url}/x402/verify`, {
274
+ const response = await fetch(`${this.url}/x402/verify`, {
245
275
  method: "POST",
246
276
  headers: this.buildHeaders(),
247
277
  body: JSON.stringify({ paymentPayload, paymentRequirements }),
278
+ redirect: "manual",
248
279
  signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
249
280
  });
281
+ throwIfRedirect(response);
250
282
  if (!response.ok) {
251
283
  const body = await readErrorBody(response);
252
284
  throw new chunkKZZY5LS6_cjs.X402RuntimeError(
@@ -274,7 +306,7 @@ var FacilitatorClient = class {
274
306
  const paymentPayload = decodePaymentHeader(paymentHeader);
275
307
  let json;
276
308
  try {
277
- const response = await fetch(`${this.config.url}/x402/settle`, {
309
+ const response = await fetch(`${this.url}/x402/settle`, {
278
310
  method: "POST",
279
311
  headers: this.buildHeaders(),
280
312
  // settleAmount only travels for metered routes; the settle token
@@ -289,8 +321,10 @@ var FacilitatorClient = class {
289
321
  settleToken: options.settleToken
290
322
  }
291
323
  }),
324
+ redirect: "manual",
292
325
  signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
293
326
  });
327
+ throwIfRedirect(response);
294
328
  if (!response.ok) {
295
329
  const body = await readErrorBody(response);
296
330
  if (body.error === void 0 && response.status >= 500) {
@@ -339,16 +373,22 @@ var FacilitatorClient = class {
339
373
  throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
340
374
  }
341
375
  }
376
+ /**
377
+ * @deprecated The Parallel facilitator no longer serves `/x402/pay`, so this
378
+ * call fails with `FACILITATOR_UNAVAILABLE`. Use `verify`, then `settle`.
379
+ */
342
380
  async pay(paymentHeader) {
343
381
  const headers = this.buildHeaders();
344
382
  headers["X-PAYMENT"] = paymentHeader;
345
383
  let json;
346
384
  try {
347
- const response = await fetch(`${this.config.url}/x402/pay`, {
385
+ const response = await fetch(`${this.url}/x402/pay`, {
348
386
  method: "POST",
349
387
  headers,
388
+ redirect: "manual",
350
389
  signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
351
390
  });
391
+ throwIfRedirect(response);
352
392
  if (!response.ok) {
353
393
  throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
354
394
  }
@@ -367,8 +407,8 @@ var FacilitatorClient = class {
367
407
  const headers = {
368
408
  "Content-Type": "application/json"
369
409
  };
370
- if (this.config.apiKey) {
371
- headers["X-API-Key"] = this.config.apiKey;
410
+ if (this.apiKey) {
411
+ headers["X-API-Key"] = this.apiKey;
372
412
  }
373
413
  return headers;
374
414
  }
@@ -652,12 +692,13 @@ function paymentRequiredResponse(url, config, resolvedTokens, price, refusal) {
652
692
  });
653
693
  }
654
694
  function createPaymentGate(config) {
655
- try {
656
- paymentMiddlewareConfigSchema.parse(config);
657
- } catch (cause) {
695
+ const checked = paymentMiddlewareConfigSchema.safeParse(config);
696
+ if (!checked.success) {
697
+ const issue = checked.error.issues[0];
698
+ const where = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
658
699
  throw new chunkKZZY5LS6_cjs.X402ConfigError(
659
- "Invalid paymentMiddleware config \u2014 check your routes, facilitator URL and payTo addresses",
660
- cause
700
+ `Invalid paymentMiddleware config${where}: ${issue?.message ?? "check your routes, facilitator URL and payTo addresses"}`,
701
+ checked.error
661
702
  );
662
703
  }
663
704
  const client = new FacilitatorClient(config.facilitator);
package/dist/express.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkNA7245L5_cjs = require('./chunk-NA7245L5.cjs');
3
+ var chunkSNW5Z5HB_cjs = require('./chunk-SNW5Z5HB.cjs');
4
4
  require('./chunk-KZZY5LS6.cjs');
5
5
 
6
6
  // src/express/adapter.ts
@@ -108,8 +108,8 @@ var ExpressAdapter = class {
108
108
  return originalEnd(...args);
109
109
  });
110
110
  next();
111
- const outcome = await chunkNA7245L5_cjs.raceTimeout(endPromise, options?.timeoutMs);
112
- if (outcome === chunkNA7245L5_cjs.HANDLER_TIMEOUT) {
111
+ const outcome = await chunkSNW5Z5HB_cjs.raceTimeout(endPromise, options?.timeoutMs);
112
+ if (outcome === chunkSNW5Z5HB_cjs.HANDLER_TIMEOUT) {
113
113
  discarded = true;
114
114
  return {
115
115
  status: 504,
@@ -146,7 +146,7 @@ var ExpressAdapter = class {
146
146
  const chunk = chunkBytes(args);
147
147
  if (chunk) bytes.push(chunk);
148
148
  }
149
- return chunkNA7245L5_cjs.parseJsonBody(
149
+ return chunkSNW5Z5HB_cjs.parseJsonBody(
150
150
  contentType,
151
151
  Buffer.concat(bytes).toString("utf8")
152
152
  );
@@ -170,7 +170,7 @@ var ExpressAdapter = class {
170
170
 
171
171
  // src/express/index.ts
172
172
  function paymentMiddleware(config) {
173
- const middleware = chunkNA7245L5_cjs.createPaymentMiddleware(config);
173
+ const middleware = chunkSNW5Z5HB_cjs.createPaymentMiddleware(config);
174
174
  return async (req, res, next) => {
175
175
  const result = await middleware(new ExpressAdapter(req, res, next));
176
176
  if ("pass" in result) {
package/dist/express.js CHANGED
@@ -1,4 +1,4 @@
1
- import { raceTimeout, HANDLER_TIMEOUT, parseJsonBody, createPaymentMiddleware } from './chunk-SA5ULSG2.js';
1
+ import { raceTimeout, HANDLER_TIMEOUT, parseJsonBody, createPaymentMiddleware } from './chunk-GXPUE3ES.js';
2
2
  import './chunk-QAECRSWF.js';
3
3
 
4
4
  // src/express/adapter.ts
package/dist/fastify.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkNA7245L5_cjs = require('./chunk-NA7245L5.cjs');
3
+ var chunkSNW5Z5HB_cjs = require('./chunk-SNW5Z5HB.cjs');
4
4
  require('./chunk-KZZY5LS6.cjs');
5
5
  var fp = require('fastify-plugin');
6
6
 
@@ -37,7 +37,7 @@ var FastifyAdapter = class {
37
37
  // src/fastify/index.ts
38
38
  function payloadJson(contentType, payload) {
39
39
  const text = typeof payload === "string" ? payload : payload instanceof Uint8Array ? Buffer.from(payload).toString("utf8") : void 0;
40
- return chunkNA7245L5_cjs.parseJsonBody(contentType, text);
40
+ return chunkSNW5Z5HB_cjs.parseJsonBody(contentType, text);
41
41
  }
42
42
  function refuse(reply, result) {
43
43
  const json = JSON.stringify(result.body);
@@ -54,11 +54,11 @@ function headerValue(value) {
54
54
  }
55
55
  function paymentMiddleware(config) {
56
56
  return fp__default.default(async (fastify) => {
57
- const gate = chunkNA7245L5_cjs.createPaymentGate(config);
57
+ const gate = chunkSNW5Z5HB_cjs.createPaymentGate(config);
58
58
  const verifiedMap = /* @__PURE__ */ new WeakMap();
59
59
  const rawWriters = /* @__PURE__ */ new WeakMap();
60
60
  const needsBody = (req) => {
61
- const route = chunkNA7245L5_cjs.matchRoute(
61
+ const route = chunkSNW5Z5HB_cjs.matchRoute(
62
62
  new FastifyAdapter(req).getPath(),
63
63
  config.routes
64
64
  );
@@ -111,29 +111,29 @@ function paymentMiddleware(config) {
111
111
  if (!verified) return payload;
112
112
  let raw;
113
113
  if (verified.scheme === "upto") {
114
- raw = headerValue(reply.getHeader(chunkNA7245L5_cjs.SETTLE_AMOUNT_HEADER));
115
- reply.removeHeader(chunkNA7245L5_cjs.SETTLE_AMOUNT_HEADER);
114
+ raw = headerValue(reply.getHeader(chunkSNW5Z5HB_cjs.SETTLE_AMOUNT_HEADER));
115
+ reply.removeHeader(chunkSNW5Z5HB_cjs.SETTLE_AMOUNT_HEADER);
116
116
  }
117
117
  if (reply.statusCode < 200 || reply.statusCode >= 300) {
118
118
  return payload;
119
119
  }
120
120
  let settleOptions;
121
121
  if (verified.scheme === "upto") {
122
- const resolved = await chunkNA7245L5_cjs.resolveCharge(verified, raw, {
122
+ const resolved = await chunkSNW5Z5HB_cjs.resolveCharge(verified, raw, {
123
123
  status: reply.statusCode,
124
124
  header: (name) => headerValue(reply.getHeader(name)),
125
125
  body: async () => payloadJson(headerValue(reply.getHeader("content-type")), payload)
126
126
  });
127
127
  if (resolved.kind === "invalid") {
128
- return refuse(reply, chunkNA7245L5_cjs.invalidSettleAmountResponse(resolved.reason));
128
+ return refuse(reply, chunkSNW5Z5HB_cjs.invalidSettleAmountResponse(resolved.reason));
129
129
  }
130
130
  if (resolved.kind === "skip") return payload;
131
131
  settleOptions = { amount: resolved.amount };
132
132
  }
133
- const outcome = await chunkNA7245L5_cjs.settleVerified(verified, settleOptions);
133
+ const outcome = await chunkSNW5Z5HB_cjs.settleVerified(verified, settleOptions);
134
134
  if (!outcome.settled)
135
- return refuse(reply, chunkNA7245L5_cjs.settlementFailureResponse(outcome));
136
- reply.header("payment-response", chunkNA7245L5_cjs.encodeBase64(outcome.confirmation));
135
+ return refuse(reply, chunkSNW5Z5HB_cjs.settlementFailureResponse(outcome));
136
+ reply.header("payment-response", chunkSNW5Z5HB_cjs.encodeBase64(outcome.confirmation));
137
137
  reply.header("access-control-expose-headers", "payment-response");
138
138
  return payload;
139
139
  });
package/dist/fastify.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createPaymentGate, SETTLE_AMOUNT_HEADER, resolveCharge, invalidSettleAmountResponse, settleVerified, settlementFailureResponse, encodeBase64, matchRoute, parseJsonBody } from './chunk-SA5ULSG2.js';
1
+ import { createPaymentGate, SETTLE_AMOUNT_HEADER, resolveCharge, invalidSettleAmountResponse, settleVerified, settlementFailureResponse, encodeBase64, matchRoute, parseJsonBody } from './chunk-GXPUE3ES.js';
2
2
  import './chunk-QAECRSWF.js';
3
3
  import fp from 'fastify-plugin';
4
4
 
package/dist/hono.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkNA7245L5_cjs = require('./chunk-NA7245L5.cjs');
3
+ var chunkSNW5Z5HB_cjs = require('./chunk-SNW5Z5HB.cjs');
4
4
  require('./chunk-KZZY5LS6.cjs');
5
5
 
6
6
  // src/hono/adapter.ts
@@ -30,7 +30,7 @@ var HonoAdapter = class {
30
30
  // Hono caches the parsed body on the request, so the handler can still read
31
31
  // it after the price function did.
32
32
  async getBody() {
33
- if (!chunkNA7245L5_cjs.isJsonContentType(this.c.req.header("content-type"))) return void 0;
33
+ if (!chunkSNW5Z5HB_cjs.isJsonContentType(this.c.req.header("content-type"))) return void 0;
34
34
  try {
35
35
  return await this.c.req.json();
36
36
  } catch {
@@ -38,8 +38,8 @@ var HonoAdapter = class {
38
38
  }
39
39
  }
40
40
  async runHandler(options) {
41
- const outcome = await chunkNA7245L5_cjs.raceTimeout(this.next(), options?.timeoutMs);
42
- if (outcome === chunkNA7245L5_cjs.HANDLER_TIMEOUT) {
41
+ const outcome = await chunkSNW5Z5HB_cjs.raceTimeout(this.next(), options?.timeoutMs);
42
+ if (outcome === chunkSNW5Z5HB_cjs.HANDLER_TIMEOUT) {
43
43
  return {
44
44
  status: 504,
45
45
  setResponseHeader: () => {
@@ -64,7 +64,7 @@ var HonoAdapter = class {
64
64
  getResponseHeader: (name) => res.headers.get(name) ?? void 0,
65
65
  removeResponseHeader: (name) => res.headers.delete(name),
66
66
  // Read from a clone: the response body streams once, to the client.
67
- getResponseBody: () => chunkNA7245L5_cjs.jsonFromResponse(res),
67
+ getResponseBody: () => chunkSNW5Z5HB_cjs.jsonFromResponse(res),
68
68
  sendResponse: () => {
69
69
  },
70
70
  // The response is replaced: release its body (and whatever upstream
@@ -79,7 +79,7 @@ var HonoAdapter = class {
79
79
 
80
80
  // src/hono/index.ts
81
81
  function paymentMiddleware(config) {
82
- const middleware = chunkNA7245L5_cjs.createPaymentMiddleware(config);
82
+ const middleware = chunkSNW5Z5HB_cjs.createPaymentMiddleware(config);
83
83
  return async (c, next) => {
84
84
  const result = await middleware(new HonoAdapter(c, next));
85
85
  if ("pass" in result) {
package/dist/hono.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isJsonContentType, raceTimeout, HANDLER_TIMEOUT, jsonFromResponse, createPaymentMiddleware } from './chunk-SA5ULSG2.js';
1
+ import { isJsonContentType, raceTimeout, HANDLER_TIMEOUT, jsonFromResponse, createPaymentMiddleware } from './chunk-GXPUE3ES.js';
2
2
  import './chunk-QAECRSWF.js';
3
3
 
4
4
  // src/hono/adapter.ts
package/dist/index.cjs CHANGED
@@ -1,65 +1,65 @@
1
1
  'use strict';
2
2
 
3
- var chunkNA7245L5_cjs = require('./chunk-NA7245L5.cjs');
3
+ var chunkSNW5Z5HB_cjs = require('./chunk-SNW5Z5HB.cjs');
4
4
  var chunkKZZY5LS6_cjs = require('./chunk-KZZY5LS6.cjs');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "FacilitatorClient", {
9
9
  enumerable: true,
10
- get: function () { return chunkNA7245L5_cjs.FacilitatorClient; }
10
+ get: function () { return chunkSNW5Z5HB_cjs.FacilitatorClient; }
11
11
  });
12
12
  Object.defineProperty(exports, "PARALLEL_TOKENS", {
13
13
  enumerable: true,
14
- get: function () { return chunkNA7245L5_cjs.PARALLEL_TOKENS; }
14
+ get: function () { return chunkSNW5Z5HB_cjs.PARALLEL_TOKENS; }
15
15
  });
16
16
  Object.defineProperty(exports, "SETTLE_AMOUNT_HEADER", {
17
17
  enumerable: true,
18
- get: function () { return chunkNA7245L5_cjs.SETTLE_AMOUNT_HEADER; }
18
+ get: function () { return chunkSNW5Z5HB_cjs.SETTLE_AMOUNT_HEADER; }
19
19
  });
20
20
  Object.defineProperty(exports, "createPaymentGate", {
21
21
  enumerable: true,
22
- get: function () { return chunkNA7245L5_cjs.createPaymentGate; }
22
+ get: function () { return chunkSNW5Z5HB_cjs.createPaymentGate; }
23
23
  });
24
24
  Object.defineProperty(exports, "createPaymentMiddleware", {
25
25
  enumerable: true,
26
- get: function () { return chunkNA7245L5_cjs.createPaymentMiddleware; }
26
+ get: function () { return chunkSNW5Z5HB_cjs.createPaymentMiddleware; }
27
27
  });
28
28
  Object.defineProperty(exports, "encodeBase64", {
29
29
  enumerable: true,
30
- get: function () { return chunkNA7245L5_cjs.encodeBase64; }
30
+ get: function () { return chunkSNW5Z5HB_cjs.encodeBase64; }
31
31
  });
32
32
  Object.defineProperty(exports, "getDefaultAcceptedTokens", {
33
33
  enumerable: true,
34
- get: function () { return chunkNA7245L5_cjs.getDefaultAcceptedTokens; }
34
+ get: function () { return chunkSNW5Z5HB_cjs.getDefaultAcceptedTokens; }
35
35
  });
36
36
  Object.defineProperty(exports, "parsePrice", {
37
37
  enumerable: true,
38
- get: function () { return chunkNA7245L5_cjs.parsePrice; }
38
+ get: function () { return chunkSNW5Z5HB_cjs.parsePrice; }
39
39
  });
40
40
  Object.defineProperty(exports, "resolveCharge", {
41
41
  enumerable: true,
42
- get: function () { return chunkNA7245L5_cjs.resolveCharge; }
42
+ get: function () { return chunkSNW5Z5HB_cjs.resolveCharge; }
43
43
  });
44
44
  Object.defineProperty(exports, "resolveSettleAmount", {
45
45
  enumerable: true,
46
- get: function () { return chunkNA7245L5_cjs.resolveSettleAmount; }
46
+ get: function () { return chunkSNW5Z5HB_cjs.resolveSettleAmount; }
47
47
  });
48
48
  Object.defineProperty(exports, "settleVerified", {
49
49
  enumerable: true,
50
- get: function () { return chunkNA7245L5_cjs.settleVerified; }
50
+ get: function () { return chunkSNW5Z5HB_cjs.settleVerified; }
51
51
  });
52
52
  Object.defineProperty(exports, "settlementFailureResponse", {
53
53
  enumerable: true,
54
- get: function () { return chunkNA7245L5_cjs.settlementFailureResponse; }
54
+ get: function () { return chunkSNW5Z5HB_cjs.settlementFailureResponse; }
55
55
  });
56
56
  Object.defineProperty(exports, "toEip155Network", {
57
57
  enumerable: true,
58
- get: function () { return chunkNA7245L5_cjs.toEip155Network; }
58
+ get: function () { return chunkSNW5Z5HB_cjs.toEip155Network; }
59
59
  });
60
60
  Object.defineProperty(exports, "validateAddress", {
61
61
  enumerable: true,
62
- get: function () { return chunkNA7245L5_cjs.validateAddress; }
62
+ get: function () { return chunkSNW5Z5HB_cjs.validateAddress; }
63
63
  });
64
64
  Object.defineProperty(exports, "X402ConfigError", {
65
65
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -3,10 +3,15 @@ export { i as MeterContext, j as MeterFunction, k as PaymentFailure, l as Paymen
3
3
  import { Address } from 'viem';
4
4
 
5
5
  declare class FacilitatorClient {
6
- private readonly config;
6
+ private readonly url;
7
+ private readonly apiKey;
7
8
  constructor(config: FacilitatorConfig);
8
9
  verify(paymentHeader: string, paymentRequirements: FacilitatorPaymentRequirements): Promise<VerifyResult>;
9
10
  settle(paymentHeader: string, paymentRequirements: FacilitatorPaymentRequirements, options?: FacilitatorSettleOptions): Promise<FacilitatorResponse>;
11
+ /**
12
+ * @deprecated The Parallel facilitator no longer serves `/x402/pay`, so this
13
+ * call fails with `FACILITATOR_UNAVAILABLE`. Use `verify`, then `settle`.
14
+ */
10
15
  pay(paymentHeader: string): Promise<FacilitatorResponse>;
11
16
  private buildHeaders;
12
17
  }
package/dist/index.d.ts CHANGED
@@ -3,10 +3,15 @@ export { i as MeterContext, j as MeterFunction, k as PaymentFailure, l as Paymen
3
3
  import { Address } from 'viem';
4
4
 
5
5
  declare class FacilitatorClient {
6
- private readonly config;
6
+ private readonly url;
7
+ private readonly apiKey;
7
8
  constructor(config: FacilitatorConfig);
8
9
  verify(paymentHeader: string, paymentRequirements: FacilitatorPaymentRequirements): Promise<VerifyResult>;
9
10
  settle(paymentHeader: string, paymentRequirements: FacilitatorPaymentRequirements, options?: FacilitatorSettleOptions): Promise<FacilitatorResponse>;
11
+ /**
12
+ * @deprecated The Parallel facilitator no longer serves `/x402/pay`, so this
13
+ * call fails with `FACILITATOR_UNAVAILABLE`. Use `verify`, then `settle`.
14
+ */
10
15
  pay(paymentHeader: string): Promise<FacilitatorResponse>;
11
16
  private buildHeaders;
12
17
  }
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { FacilitatorClient, PARALLEL_TOKENS, SETTLE_AMOUNT_HEADER, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, parsePrice, resolveCharge, resolveSettleAmount, settleVerified, settlementFailureResponse, toEip155Network, validateAddress } from './chunk-SA5ULSG2.js';
1
+ export { FacilitatorClient, PARALLEL_TOKENS, SETTLE_AMOUNT_HEADER, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, parsePrice, resolveCharge, resolveSettleAmount, settleVerified, settlementFailureResponse, toEip155Network, validateAddress } from './chunk-GXPUE3ES.js';
2
2
  export { X402ConfigError, X402PriceRejectedError, X402RuntimeError, X402_ERROR_CODES, isPriceRejection } from './chunk-QAECRSWF.js';
package/dist/next.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkNA7245L5_cjs = require('./chunk-NA7245L5.cjs');
3
+ var chunkSNW5Z5HB_cjs = require('./chunk-SNW5Z5HB.cjs');
4
4
  require('./chunk-KZZY5LS6.cjs');
5
5
  var server = require('next/server');
6
6
 
@@ -25,7 +25,7 @@ var NextAdapter = class {
25
25
  // Read from a clone: a request body is a stream that can be consumed once,
26
26
  // and the handler still needs the original.
27
27
  async getBody() {
28
- if (!chunkNA7245L5_cjs.isJsonContentType(this.req.headers.get("content-type") ?? void 0))
28
+ if (!chunkSNW5Z5HB_cjs.isJsonContentType(this.req.headers.get("content-type") ?? void 0))
29
29
  return void 0;
30
30
  try {
31
31
  return await this.req.clone().json();
@@ -43,7 +43,7 @@ function fromResult(result) {
43
43
  });
44
44
  }
45
45
  function withPayment(config, handler) {
46
- const gate = chunkNA7245L5_cjs.createPaymentGate(config);
46
+ const gate = chunkSNW5Z5HB_cjs.createPaymentGate(config);
47
47
  return async (req) => {
48
48
  const gateResult = await gate(new NextAdapter(req));
49
49
  if (gateResult.type === "pass") {
@@ -52,11 +52,11 @@ function withPayment(config, handler) {
52
52
  if (gateResult.type === "error") return fromResult(gateResult.result);
53
53
  let response;
54
54
  try {
55
- const outcome = await chunkNA7245L5_cjs.raceTimeout(
55
+ const outcome = await chunkSNW5Z5HB_cjs.raceTimeout(
56
56
  handler(req),
57
57
  gateResult.handlerTimeoutMs
58
58
  );
59
- if (outcome === chunkNA7245L5_cjs.HANDLER_TIMEOUT) {
59
+ if (outcome === chunkSNW5Z5HB_cjs.HANDLER_TIMEOUT) {
60
60
  return new server.NextResponse(null, { status: 504 });
61
61
  }
62
62
  response = outcome;
@@ -65,10 +65,10 @@ function withPayment(config, handler) {
65
65
  }
66
66
  let raw;
67
67
  if (gateResult.scheme === "upto") {
68
- raw = response.headers.get(chunkNA7245L5_cjs.SETTLE_AMOUNT_HEADER) ?? void 0;
68
+ raw = response.headers.get(chunkSNW5Z5HB_cjs.SETTLE_AMOUNT_HEADER) ?? void 0;
69
69
  if (raw !== void 0) {
70
70
  const headers = new Headers(response.headers);
71
- headers.delete(chunkNA7245L5_cjs.SETTLE_AMOUNT_HEADER);
71
+ headers.delete(chunkSNW5Z5HB_cjs.SETTLE_AMOUNT_HEADER);
72
72
  response = new server.NextResponse(response.body, {
73
73
  status: response.status,
74
74
  headers
@@ -79,24 +79,24 @@ function withPayment(config, handler) {
79
79
  let settleOptions;
80
80
  if (gateResult.scheme === "upto") {
81
81
  const handled = response;
82
- const resolved = await chunkNA7245L5_cjs.resolveCharge(gateResult, raw, {
82
+ const resolved = await chunkSNW5Z5HB_cjs.resolveCharge(gateResult, raw, {
83
83
  status: handled.status,
84
84
  header: (name) => handled.headers.get(name) ?? void 0,
85
85
  // Read from a clone: the body streams once, to the client.
86
- body: () => chunkNA7245L5_cjs.jsonFromResponse(handled)
86
+ body: () => chunkSNW5Z5HB_cjs.jsonFromResponse(handled)
87
87
  });
88
88
  if (resolved.kind === "invalid") {
89
89
  handled.body?.cancel().catch(() => {
90
90
  });
91
- return fromResult(chunkNA7245L5_cjs.invalidSettleAmountResponse(resolved.reason));
91
+ return fromResult(chunkSNW5Z5HB_cjs.invalidSettleAmountResponse(resolved.reason));
92
92
  }
93
93
  if (resolved.kind === "skip") return response;
94
94
  settleOptions = { amount: resolved.amount };
95
95
  }
96
- const outcome = await chunkNA7245L5_cjs.settleVerified(gateResult, settleOptions);
96
+ const outcome = await chunkSNW5Z5HB_cjs.settleVerified(gateResult, settleOptions);
97
97
  if (outcome.settled) {
98
98
  const headers = new Headers(response.headers);
99
- headers.set("payment-response", chunkNA7245L5_cjs.encodeBase64(outcome.confirmation));
99
+ headers.set("payment-response", chunkSNW5Z5HB_cjs.encodeBase64(outcome.confirmation));
100
100
  headers.set("access-control-expose-headers", "payment-response");
101
101
  return new server.NextResponse(response.body, {
102
102
  status: response.status,
@@ -105,7 +105,7 @@ function withPayment(config, handler) {
105
105
  }
106
106
  response.body?.cancel().catch(() => {
107
107
  });
108
- return fromResult(chunkNA7245L5_cjs.settlementFailureResponse(outcome));
108
+ return fromResult(chunkSNW5Z5HB_cjs.settlementFailureResponse(outcome));
109
109
  }
110
110
  return response;
111
111
  };
package/dist/next.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isJsonContentType, createPaymentGate, raceTimeout, HANDLER_TIMEOUT, SETTLE_AMOUNT_HEADER, resolveCharge, jsonFromResponse, invalidSettleAmountResponse, settleVerified, encodeBase64, settlementFailureResponse } from './chunk-SA5ULSG2.js';
1
+ import { isJsonContentType, createPaymentGate, raceTimeout, HANDLER_TIMEOUT, SETTLE_AMOUNT_HEADER, resolveCharge, jsonFromResponse, invalidSettleAmountResponse, settleVerified, encodeBase64, settlementFailureResponse } from './chunk-GXPUE3ES.js';
2
2
  import './chunk-QAECRSWF.js';
3
3
  import { NextResponse } from 'next/server';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parallel-protocol/x402",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Parallel Protocol x402 payment middleware — Express, Next.js, Fastify, Hono",
5
5
  "author": "Parallel Protocol <contact@parallel.best>",
6
6
  "repository": {