@parallel-protocol/x402 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,11 +24,12 @@ Agent Merchant (this SDK) Facilitator
24
24
  | | |
25
25
  |── GET /api/data ──────────────>| |
26
26
  | payment-signature: <base64> |── POST /x402/verify ───────>|
27
- | |<─ { isValid: true } ────────|
27
+ | |<─ { isValid, settleToken } ─|
28
28
  | | |
29
29
  | | [handler runs] |
30
30
  | | |
31
31
  | |── POST /x402/settle ───────>|
32
+ | | (+ settleToken) |
32
33
  | |<─ { txHash, route, ... } ───|
33
34
  | | |
34
35
  |<─ 200 + payment-response ──────| |
@@ -38,8 +39,10 @@ Agent Merchant (this SDK) Facilitator
38
39
  **Key properties:**
39
40
  - The handler only runs after the signature is verified.
40
41
  - The on-chain settlement happens after the handler produces a 2xx response — the agent is not charged on errors.
41
- - If settlement fails (on-chain revert, facilitator timeout), the middleware returns `402` and discards the handler's response.
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.)
42
+ - The settle is bound to the merchant that verified: `POST /x402/verify` hands the middleware a settle token that `POST /x402/settle` must present. The token stays in memory for the life of the request and never reaches the agent.
43
+ - If settlement fails, the middleware discards the handler's response and returns `402` with `{ error: "PAYMENT_SETTLEMENT_FAILED", reason, message }`: `reason` is the facilitator's code (`INSUFFICIENT_BALANCE`, `AUTHORIZATION_USED`, `PAYMENT_EXPIRED`, ...) or this SDK's (`SETTLE_TIMEOUT`, `FACILITATOR_UNAVAILABLE`, `FACILITATOR_INVALID_RESPONSE`, `AMOUNT_MISMATCH`, `UNKNOWN`), `message` the facilitator's text or this SDK's, and the facilitator's `Retry-After` is copied when it sent one. A settle that timed out or could not reach the facilitator is retried once first: the same settle is idempotent, and the facilitator answers the recorded outcome if the first attempt went through.
44
+ - A metered settle must settle the amount the handler asked for: a confirmation for another amount is refused and the response discarded.
45
+ - 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.) Metered routes get their announced window minus two minutes kept for the settlement and its retry.
43
46
  - 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.
44
47
  - CORS preflight (`OPTIONS`) is always passed through without challenge.
45
48
 
@@ -201,7 +204,7 @@ interface PaymentMiddlewareConfig {
201
204
  | `description` | `string` | No | Human-readable description of the resource, included in the 402 body. |
202
205
  | `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
206
  | `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). |
204
- | `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. |
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. |
205
208
 
206
209
  Route matching uses **longest prefix wins**: `/api/v1/users` will match a `/api/v1` route config before a `/api` one.
207
210
 
@@ -297,6 +300,7 @@ When no valid payment is present, the middleware returns HTTP 402 with a `paymen
297
300
  interface PaymentRequired {
298
301
  x402Version: 2;
299
302
  error?: string; // present on re-challenge (e.g. "INVALID_SIGNATURE")
303
+ message?: string; // the facilitator's text for `error`, when it sent one
300
304
  resource: {
301
305
  url: string;
302
306
  description?: string;
@@ -355,6 +359,7 @@ interface PaymentConfirmation {
355
359
  gasSponsored: boolean;
356
360
  networkId: string; // "eip155:8453"
357
361
  amountSettled?: string; // metered routes: what was actually charged (smallest unit)
362
+ alreadySettled?: true; // the facilitator answered from a recorded outcome, this call broadcast nothing
358
363
  }
359
364
  ```
360
365
 
@@ -371,7 +376,7 @@ routes: {
371
376
  "/v1/chat/completions": {
372
377
  scheme: "upto",
373
378
  price: "2", // the cap, in USDp
374
- maxTimeoutSeconds: 600, // the handler may take up to ~9 minutes
379
+ maxTimeoutSeconds: 600, // the handler may take up to 8 minutes
375
380
  network: "base",
376
381
  payTo: "0xYourAddress",
377
382
  },
@@ -423,7 +428,7 @@ Instead of a header the handler sets, a route may carry a `meter` function that
423
428
 
424
429
  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.
425
430
 
426
- 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.
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.
427
432
 
428
433
  ---
429
434
 
@@ -689,7 +694,7 @@ if (result.type === "pass") {
689
694
  }
690
695
  ```
691
696
 
692
- `createPaymentGate` only verifies; it does not run the handler. Use `createPaymentMiddleware` if you want the full verify → run → settle flow managed automatically.
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.
693
698
 
694
699
  ---
695
700
 
@@ -711,21 +716,25 @@ const requirements: FacilitatorPaymentRequirements = {
711
716
  network: "base", // plain chain slug
712
717
  };
713
718
 
714
- // Phase 1: verify signature + reserve nonce (no on-chain tx)
715
- await client.verify(paymentHeader, requirements); // throws X402RuntimeError on failure
719
+ // Phase 1: verify signature + reserve nonce (no on-chain tx). The facilitator
720
+ // hands back the settle token this payment must be settled with.
721
+ const { settleToken } = await client.verify(paymentHeader, requirements); // throws X402RuntimeError on failure
716
722
 
717
723
  // Phase 2: submit on-chain, return settlement result
718
- const result = await client.settle(paymentHeader, requirements);
724
+ const result = await client.settle(paymentHeader, requirements, { settleToken });
719
725
  if (result.success) {
720
- console.log(result.txHash); // on-chain tx hash
721
- console.log(result.route); // "A", "B", ...
722
- console.log(result.gasSponsored); // boolean
726
+ console.log(result.txHash); // on-chain tx hash
727
+ console.log(result.route); // "A", "B", ...
728
+ console.log(result.gasSponsored); // boolean
729
+ console.log(result.alreadySettled); // true when an earlier settle already did it
723
730
  } else {
724
- console.error(result.error.code); // e.g. "SUBMISSION_FAILED"
731
+ console.error(result.error.code); // e.g. "INSUFFICIENT_BALANCE"
732
+ console.error(result.error.message); // the facilitator's text
733
+ console.error(result.error.status); // its HTTP status, result.error.retryAfter when it sent one
725
734
  }
726
735
  ```
727
736
 
728
- `verify` uses a 10-second timeout. `settle` uses a 60-second timeout to accommodate on-chain confirmation latency. `verify` throws on any failure; `settle` returns a `FacilitatorResponse` union on facilitator errors and throws `X402RuntimeError` on connectivity issues, an undecodable payment header, or a malformed facilitator response.
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.
729
738
 
730
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.
731
740
 
@@ -753,23 +762,47 @@ Refusals `chatCompletionCap` makes, all before payment:
753
762
 
754
763
  | `X402RuntimeError.code` | Meaning |
755
764
  |-------------------------|---------|
756
- | `FACILITATOR_UNAVAILABLE` | Facilitator timed out (10s on verify, 60s on settle) or was unreachable |
757
- | `FACILITATOR_INVALID_RESPONSE` | Facilitator returned an unexpected response shape |
765
+ | `FACILITATOR_UNAVAILABLE` | Facilitator timed out on verify (10s) or was unreachable |
766
+ | `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 |
758
768
  | `INVALID_PAYMENT` | `payment-signature` / `X-PAYMENT` header could not be base64-decoded |
759
769
 
760
770
  ### Propagated from the facilitator
761
771
 
762
- When the facilitator rejects a payment, its error code is forwarded as the `error` field on the re-issued 402 body. Common values:
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:
763
773
 
764
774
  | Code | Meaning |
765
775
  |------|---------|
776
+ | `INVALID_PAYLOAD` | The payload does not have the shape of its route, or carries a field another route would read |
766
777
  | `INVALID_SIGNATURE` | EIP-712 signature recovery failed |
767
778
  | `INVALID_NONCE` | Nonce already used (replay detected) |
768
779
  | `PAYMENT_EXPIRED` | `validBefore` has passed |
769
- | `INSUFFICIENT_AMOUNT` | Signed amount < required amount |
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` |
770
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 |
771
787
  | `RATE_LIMIT_EXCEEDED` | Signer has exceeded the hourly transaction limit |
772
- | `PARALLELIZER_PAUSED` | On-chain router temporarily paused (swap routes only) |
788
+
789
+ 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
+
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.
804
+
805
+ `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.
773
806
 
774
807
  ---
775
808
 
@@ -4,7 +4,9 @@
4
4
  var X402_ERROR_CODES = {
5
5
  FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
6
6
  FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
7
- INVALID_PAYMENT: "INVALID_PAYMENT"
7
+ INVALID_PAYMENT: "INVALID_PAYMENT",
8
+ /** The facilitator did not answer a settle within the client's timeout: the payment may still settle. */
9
+ SETTLE_TIMEOUT: "SETTLE_TIMEOUT"
8
10
  };
9
11
  var X402ConfigError = class extends Error {
10
12
  constructor(message, cause) {
@@ -14,12 +16,21 @@ var X402ConfigError = class extends Error {
14
16
  }
15
17
  };
16
18
  var X402RuntimeError = class extends Error {
17
- constructor(code) {
19
+ constructor(code, extra) {
18
20
  super(code);
19
21
  this.code = code;
20
22
  this.name = "X402RuntimeError";
23
+ if (extra?.detail !== void 0) this.detail = extra.detail;
24
+ if (extra?.status !== void 0) this.status = extra.status;
25
+ if (extra?.retryAfter !== void 0) this.retryAfter = extra.retryAfter;
21
26
  }
22
27
  code;
28
+ /** The facilitator's message for this code, when it answered with one. */
29
+ detail;
30
+ /** The facilitator's HTTP status, when the error comes from its answer. */
31
+ status;
32
+ /** The facilitator's `Retry-After` header, when it sent one. */
33
+ retryAfter;
23
34
  };
24
35
  var X402PriceRejectedError = class extends Error {
25
36
  constructor(code, message, status = 400) {