@parallel-protocol/x402 0.8.3 → 0.10.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
@@ -201,7 +201,7 @@ interface PaymentMiddlewareConfig {
201
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. |
202
202
  | `network` | `string` | Yes | Chain slug: `"ethereum"`, `"base"`, `"avalanche"`, `"hyperevm"`. |
203
203
  | `payTo` | `Address` | Yes | The merchant's receiving address. |
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
+ | `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. It may also list the network's USDC where the escrow takes Permit2 payments (Base), see [Payments from standard x402 clients](#payments-from-standard-x402-clients-usdc-through-permit2). Any other token is rejected at construction. |
205
205
  | `description` | `string` | No | Human-readable description of the resource, included in the 402 body. |
206
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). |
207
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). |
@@ -314,8 +314,17 @@ interface PaymentRequired {
314
314
  amount: string; // smallest unit of `asset`, per its own decimals — the cap for "upto"
315
315
  payTo: string;
316
316
  maxTimeoutSeconds: number; // the route's settlement window (default 300)
317
- extra: { decimals: number }; // decimals of `asset` — payers need not guess
317
+ extra: {
318
+ decimals: number; // decimals of `asset` — payers need not guess
319
+ // on an "upto" entry paid through Permit2 (USDC): the token's EIP-712 domain and the escrow
320
+ name?: string;
321
+ version?: string;
322
+ facilitatorAddress?: string;
323
+ };
318
324
  }>;
325
+ extensions?: { // present when an entry is paid through Permit2
326
+ eip2612GasSponsoring: { info: { description: string; version: "1" }; schema: object };
327
+ };
319
328
  }
320
329
  ```
321
330
 
@@ -364,7 +373,13 @@ interface PaymentConfirmation {
364
373
  gasSponsored: boolean;
365
374
  networkId: string; // "eip155:8453"
366
375
  amountSettled?: string; // metered routes: what was actually charged (smallest unit)
376
+ amountDelivered?: string; // paid through Permit2: the USDp the merchant received (smallest unit), when the facilitator read it
367
377
  alreadySettled?: true; // the facilitator answered from a recorded outcome, this call broadcast nothing
378
+ // The fields of the standard x402 settlement response, read by clients built on the x402 SDKs:
379
+ transaction?: string; // the txHash
380
+ network?: string; // "eip155:8453"
381
+ amount?: string; // the amount charged, in the smallest unit of the paid token
382
+ payer?: string;
368
383
  }
369
384
  ```
370
385
 
@@ -374,7 +389,7 @@ The `access-control-expose-headers` header is set automatically so browsers can
374
389
 
375
390
  ## Metered routes (`upto`)
376
391
 
377
- 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`.
392
+ 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, and in USDC from standard x402 clients ([below](#payments-from-standard-x402-clients-usdc-through-permit2)). 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`.
378
393
 
379
394
  ```typescript
380
395
  routes: {
@@ -430,11 +445,36 @@ Instead of a header the handler sets, a route may carry a `meter` function that
430
445
  | `"0"` | The request is served for free, nothing is settled. |
431
446
  | `undefined` or `null` | The `payment-settle-amount` header decides, with the rules above (absent charges the cap). |
432
447
  | it throws, or returns something that is not an amount | The response is discarded and the client gets 402 `INVALID_SETTLE_AMOUNT`. |
448
+ | a bigint, on a route that accepts tokens with different decimals | Refused like a failing meter, since a bigint is in one token's unit. Return a decimal string such as `"0.75"`. |
433
449
 
434
450
  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.
435
451
 
436
452
  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.
437
453
 
454
+ ### Payments from standard x402 clients (USDC through Permit2)
455
+
456
+ Agents built on the x402 SDKs, AgentCash among them, pay a metered route with a Permit2 signature of the cap in USDC. To accept them, list the network's USDC in `acceptedTokens`:
457
+
458
+ ```typescript
459
+ import { PARALLEL_TOKENS } from "@parallel-protocol/x402";
460
+
461
+ routes: {
462
+ "/v1/chat/completions": {
463
+ scheme: "upto",
464
+ price: "2", // the cap, in USDC or in USDp
465
+ network: "base",
466
+ payTo: "0xYourReceivingAddress",
467
+ acceptedTokens: [PARALLEL_TOKENS.base.usdc, PARALLEL_TOKENS.base.usdp],
468
+ },
469
+ },
470
+ ```
471
+
472
+ The 402 lists the USDC entry first, because these clients pay the first `upto` entry, and declares the `eip2612GasSponsoring` extension, so an agent that has not approved Permit2 signs a permit instead ([402 response](#402-response-payment-required)). The escrow converts the USDC to USDp, which the merchant receives. The handler names the amount to charge as for any metered route. The `payment-response` receipt gives the USDC charged in `amountSettled`, and the USDp the merchant received in `amountDelivered` when the facilitator could read it from the settlement.
473
+
474
+ Before listing USDC, approve the escrow once for USDC from the `payTo` address, since the escrow takes the USDC back to convert it. Its address is `getUptoEscrowAddress("base")` from `@parallel-protocol/chains`. Receive on a dedicated address rather than a treasury: it bounds what the approval exposes.
475
+
476
+ The price must be a decimal string such as `"2"`, and so must the amount a `meter` returns. A bigint, or an explicit `decimals`, is in one token's unit and is refused on a route that accepts both tokens. Standard clients also refuse a cap above their spending limit, a few dollars by default, so keep the cap of such a route low.
477
+
438
478
  ---
439
479
 
440
480
  ## Per-request pricing
@@ -541,7 +581,7 @@ Without `meter`, what is charged after the response is the handler's call: set t
541
581
 
542
582
  ## Payment routes
543
583
 
544
- The facilitator supports 13 settlement routes. The route is selected automatically based on the signed payload:
584
+ The facilitator supports 14 settlement routes. The route is selected automatically based on the signed payload:
545
585
 
546
586
  | Route | Credential method(s) | tokenIn (agent pays) | tokenOut (merchant receives) | Gas est. | Notes |
547
587
  |-------|--------------------------|---------------------|------------------------------|----------|-------|
@@ -558,6 +598,7 @@ The facilitator supports 13 settlement routes. The route is selected automatical
558
598
  | **K** | `redeemWithAuthorization` + `swapExactOutputWithAuthorization` | sUSDp | non-USDC backing (exact) | ~180K | Same as I but for other Parallelizer collaterals (atomic multicall3) |
559
599
  | **L** | `partialRedeemWithAuthorization` | sUSDp (partial) + USDp | USDp or backing collateral | ~180K | Partial sUSDp redeem combined with existing USDp; delivers to merchant (atomic multicall3) |
560
600
  | **U** | `uptoWithAuthorization` | USDp (cap) | USDp (actual) | ~130K | Metered payment: the escrow pulls the signed cap, pays the merchant the settled amount and refunds the rest atomically (scheme `upto`) |
601
+ | **V** | `uptoPermit2` | USDC (cap), from a standard x402 client | USDp (actual, converted) | ~390K | Metered payment signed with Permit2: the x402 proxy pays the merchant the settled amount in USDC, which the escrow converts to USDp in the same transaction (scheme `upto`) |
561
602
 
562
603
  **Chain requirements:**
563
604
  - Routes B, D, E, G, I, K, L require the **Parallelizer** to be deployed on the target chain (Ethereum, Base, Avalanche, HyperEVM).
@@ -565,7 +606,7 @@ The facilitator supports 13 settlement routes. The route is selected automatical
565
606
  - Routes C, G, H, I, J, K, L require **sUSDp** (savings module) to be deployed on the target chain.
566
607
  - Agents pay with USDp, sUSDp or the chain's USDC. Route F takes the chain's USDC, routes E and G the chain's USDC where it is a collateral (Base and Avalanche). On these three routes any other `tokenIn` is refused at verify and at settle with `PAYMENT_METHOD_NOT_SUPPORTED`, before the signature is checked.
567
608
  - The facilitator settles on Ethereum, Base, Avalanche and HyperEVM. A payment on another chain is refused at verify and at settle with `NETWORK_MISMATCH`.
568
- - 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`.
609
+ - Routes U and V need the escrow contract, deployed on Base only. A metered payment on another chain is refused at verify with `PAYMENT_METHOD_NOT_SUPPORTED`.
569
610
  - Gas sponsorship is decided by the facilitator per payment and reported as `gasSponsored` in the settlement receipt.
570
611
  - 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.
571
612
 
@@ -784,7 +825,7 @@ What the middleware answers on a paid route instead of the handler's response:
784
825
 
785
826
  | Error class | When |
786
827
  |-------------|------|
787
- | `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 `network` of the catalog other than `ethereum`, `base`, `avalanche` or `hyperevm`, 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 |
828
+ | `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 `network` of the catalog other than `ethereum`, `base`, `avalanche` or `hyperevm`, a malformed or non-positive `price`, a `meter` on a route that is not `upto`, or an `upto` route with `maxTimeoutSeconds` above 3600, a token other than USDp and the network's USDC paid through Permit2, or a bigint price or an explicit `decimals` while it accepts tokens with different decimals. 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, returns something that is not a price, or returns a bigint on an `upto` route that accepts tokens with different decimals |
788
829
  | `X402RuntimeError` | Runtime failure; `.code` is one of the SDK codes below, or a facilitator error code propagated verbatim (see next table) |
789
830
  | `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. |
790
831
 
@@ -825,12 +866,15 @@ When the facilitator refuses a payment at verify, its code is forwarded as the `
825
866
 
826
867
  | Code | Facilitator status | Meaning | What the agent should do |
827
868
  |------|--------------------|---------|--------------------------|
828
- | `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 |
829
- | `INVALID_SIGNATURE` | 400 | EIP-712 signature recovery failed | Sign again |
869
+ | `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, or its EIP-2612 permit is not for the cap or ends before the Permit2 deadline | Sign a payment that matches one of the `accepts` entries |
870
+ | `INVALID_SIGNATURE` | 400 | EIP-712 signature recovery failed, or the payer's contract wallet refused the signature (payments through Permit2) | Sign again |
830
871
  | `INVALID_NONCE` | 409 | Nonce already used: the same payment reached the facilitator before, and may have settled | Do not sign another payment before checking with the merchant |
831
872
  | `PAYMENT_EXPIRED` | 402 | `validBefore` has passed | Sign a new payment |
873
+ | `PAYMENT_NOT_YET_VALID` | 402 | A payment through Permit2 whose `validAfter` has not passed yet | Sign again with a `validAfter` in the past |
832
874
  | `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` |
833
875
  | `INSUFFICIENT_BALANCE` | 402 | The payer does not hold the signed amount (metered payments) | Fund the wallet, sign a new payment |
876
+ | `PERMIT2_ALLOWANCE_REQUIRED` | 412 | A payment through Permit2 from a payer whose Permit2 approval for the token is below the cap, with no EIP-2612 permit or one the token refused. The message starts with `permit2_allowance_required`, the reason standard x402 clients look for | Approve Permit2 for the token once, or sign the payment with an EIP-2612 permit |
877
+ | `TOKEN_BLOCKED` | 403 | The token refuses the transfer: an address is blacklisted, or the token is paused. Nothing was taken | Pay with another token |
834
878
  | `ASSET_MISMATCH` | 400 | The route does not deliver the token the merchant requires | Pay so that the merchant receives one of the `accepts` assets |
835
879
  | `RECIPIENT_MISMATCH` | 400 | The signed recipient is not the merchant's `payTo` | Sign to the invoiced `payTo` |
836
880
  | `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 |
@@ -840,6 +884,9 @@ When the facilitator refuses a payment at verify, its code is forwarded as the `
840
884
  | `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 |
841
885
  | `PARALLELIZER_PAUSED` | 503 | The Parallelizer has closed the swap the payment needs, or its state could not be read. Nothing was taken | Pay with another token, or pay again later |
842
886
  | `SAVINGS_PAUSED` | 503 | The sUSDp savings vault the payment deposits into or redeems from is paused, or its state could not be read. Nothing was taken | Pay with another token, or pay again later |
887
+ | `MERCHANT_NOT_ENABLED` | 503 | The merchant's `payTo` has not approved the escrow for USDC yet, so a USDC payment cannot be converted (see [Payments from standard x402 clients](#payments-from-standard-x402-clients-usdc-through-permit2)). Nothing was taken | Pay with USDp, or pay again later |
888
+ | `SWAP_UNAVAILABLE` | 503 | The Parallelizer cannot convert the token now: a cap is reached or a price feed is stale. Nothing was taken | Pay with USDp, or pay again later |
889
+ | `SETTLEMENT_WOULD_FAIL` | 402 | A payment through Permit2 whose settlement, simulated at verify, would revert for another reason. Nothing was taken | Pay with another token, or sign a new payment later |
843
890
  | `SERVICE_UNAVAILABLE` | 503 | The facilitator could not read the chain or its own store | Pay again after a short wait |
844
891
 
845
892
  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)).
@@ -848,7 +895,7 @@ When the facilitator refuses at settle, the 402 body is `{ error: "PAYMENT_SETTL
848
895
 
849
896
  | Reason | Facilitator status | Meaning | What the agent should do |
850
897
  |--------|--------------------|---------|--------------------------|
851
- | `INSUFFICIENT_BALANCE` | 402 | The payer no longer holds the signed amount | Fund the wallet, sign a new payment |
898
+ | `INSUFFICIENT_BALANCE` | 402 | The payer no longer holds the signed amount, or for a payment through Permit2 the amount charged | Fund the wallet, sign a new payment |
852
899
  | `AUTHORIZATION_USED` | 409 | The authorization is already consumed or cancelled on-chain. It may have paid the merchant | Do not sign another payment before checking with the merchant |
853
900
  | `PAYMENT_EXPIRED` | 402 | The authorization expired before settlement | Sign a new payment |
854
901
  | `PAYMENT_NOT_VERIFIED` | 402 | No verified payment for this authorization, or an earlier settle of it failed. After `AUTHORIZATION_USED`, `RECEIPT_UNAVAILABLE` or `SUBMISSION_FAILED`, a repeated settle gets that code again instead. The merchant may have been paid, or may still be | Do not sign another payment before checking with the merchant |
@@ -857,7 +904,7 @@ When the facilitator refuses at settle, the 402 body is `{ error: "PAYMENT_SETTL
857
904
  | `RELAYER_UNAVAILABLE` | 503 | The facilitator is temporarily unable to settle. The payment was not taken, and the facilitator will not settle it | Retry later with a new payment, after `Retry-After` when given |
858
905
  | `PARALLELIZER_PAUSED`, `SAVINGS_PAUSED`, `SERVICE_UNAVAILABLE` | 503 | The swap or the savings vault the payment needs is paused, or its state could not be read, or the facilitator or the chain is temporarily unable to settle. This settle sent nothing, but the payment may still settle until its authorization expires, and an earlier attempt may already have settled it | Do not sign another payment before checking with the merchant |
859
906
  | `TX_REVERTED` | 500 | The transaction reverted for a reason the facilitator could not attribute | Sign a new payment |
860
- | `SWAP_PRICE_MOVED` | 402 | The Parallelizer refused the swap: its price or fee moved, between signing and settlement, beyond the margin signed with the payment. The payment was not taken, and the facilitator will not settle it | Sign a new payment |
907
+ | `SWAP_PRICE_MOVED` | 402 | The Parallelizer refused the swap: its price or fee moved beyond the payment's margin, the one signed with it, or for a payment through Permit2 the one the facilitator sets under the Parallelizer's quote. The payment was not taken, and the facilitator will not settle it | Sign a new payment |
861
908
  | `SWAP_UNAVAILABLE` | 503 | The Parallelizer refused the swap because it cannot make it now: the swap closed since the facilitator checked it, a cap or a maximum fee is reached, the amount exceeds what it can release, or a price feed is stale. The payment was not taken, and the facilitator will not settle it | Pay with another token, or later with a new payment |
862
909
  | `SUBMISSION_FAILED` | 500 | The settle failed. When the message says the outcome is unknown, a transaction may have been sent, or the facilitator could not read whether the authorization was used, and the payment may have settled or may still settle | Do not sign another payment before checking with the merchant. x402-fetch says so for this reason whatever the message. A client that reads the 402 itself may sign a new payment when the message says no transaction was submitted |
863
910
  | `RECEIPT_UNAVAILABLE` | 500 | The transaction was sent but its confirmation could not be read | Do not sign another payment before checking with the merchant |
@@ -2,6 +2,7 @@
2
2
 
3
3
  var chunkKZZY5LS6_cjs = require('./chunk-KZZY5LS6.cjs');
4
4
  var chains = require('@parallel-protocol/chains');
5
+ var paymentCore = require('@parallel-protocol/payment-core');
5
6
  var zod = require('zod');
6
7
 
7
8
  var PARALLEL_TOKENS = {
@@ -35,6 +36,9 @@ var PARALLEL_TOKENS = {
35
36
  sUSDe: "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2"
36
37
  }
37
38
  };
39
+ var USDC_EIP712_DOMAIN = {
40
+ base: { name: "USD Coin", version: "2" }
41
+ };
38
42
  function getDefaultAcceptedTokens(network) {
39
43
  const entry = PARALLEL_TOKENS[network];
40
44
  if (!entry) return void 0;
@@ -48,6 +52,12 @@ function getMeteredAcceptedTokens(network) {
48
52
  function getTokenDecimals(network, address) {
49
53
  return chains.getTokenDecimals(network, address);
50
54
  }
55
+ function hasMixedDecimals(network, tokens) {
56
+ const decimals = new Set(
57
+ tokens?.map((token) => chains.getTokenDecimals(network, token))
58
+ );
59
+ return decimals.size > 1;
60
+ }
51
61
 
52
62
  // src/utils.ts
53
63
  var EIP155_NETWORKS = {
@@ -118,6 +128,114 @@ function matchRoute(path, routes) {
118
128
  }
119
129
  return bestMatch;
120
130
  }
131
+ function permit2Extra(network, asset) {
132
+ const domain = USDC_EIP712_DOMAIN[network];
133
+ const escrow = chains.getUptoEscrowAddress(network);
134
+ if (!domain || !escrow) return void 0;
135
+ const usdc = PARALLEL_TOKENS[network].usdc;
136
+ if (usdc.toLowerCase() !== asset.toLowerCase()) return void 0;
137
+ return { ...domain, facilitatorAddress: escrow };
138
+ }
139
+ var EIP2612_GAS_SPONSORING_EXTENSION = {
140
+ eip2612GasSponsoring: {
141
+ info: {
142
+ description: "The facilitator accepts EIP-2612 gasless Permit to `Permit2` canonical contract.",
143
+ version: "1"
144
+ },
145
+ schema: {
146
+ $schema: "https://json-schema.org/draft/2020-12/schema",
147
+ type: "object",
148
+ properties: {
149
+ from: {
150
+ type: "string",
151
+ pattern: "^0x[a-fA-F0-9]{40}$",
152
+ description: "The address of the sender."
153
+ },
154
+ asset: {
155
+ type: "string",
156
+ pattern: "^0x[a-fA-F0-9]{40}$",
157
+ description: "The address of the ERC-20 token contract."
158
+ },
159
+ spender: {
160
+ type: "string",
161
+ pattern: "^0x[a-fA-F0-9]{40}$",
162
+ description: "The address of the spender (Canonical Permit2)."
163
+ },
164
+ amount: {
165
+ type: "string",
166
+ pattern: "^[0-9]+$",
167
+ description: "The amount to approve (uint256). Typically MaxUint."
168
+ },
169
+ nonce: {
170
+ type: "string",
171
+ pattern: "^[0-9]+$",
172
+ description: "The current nonce of the sender."
173
+ },
174
+ deadline: {
175
+ type: "string",
176
+ pattern: "^[0-9]+$",
177
+ description: "The timestamp at which the signature expires."
178
+ },
179
+ signature: {
180
+ type: "string",
181
+ pattern: "^0x[a-fA-F0-9]+$",
182
+ description: "The 65-byte concatenated signature (r, s, v) as a hex string."
183
+ },
184
+ version: {
185
+ type: "string",
186
+ pattern: "^[0-9]+(\\.[0-9]+)*$",
187
+ description: "Schema version identifier."
188
+ }
189
+ },
190
+ required: [
191
+ "from",
192
+ "asset",
193
+ "spender",
194
+ "amount",
195
+ "nonce",
196
+ "deadline",
197
+ "signature",
198
+ "version"
199
+ ]
200
+ }
201
+ }
202
+ };
203
+ function signedPermit(info = {}) {
204
+ const { amount, deadline, signature } = info;
205
+ return typeof amount === "string" && typeof deadline === "string" && typeof signature === "string" ? { value: amount, deadline, signature } : void 0;
206
+ }
207
+ function isStandardPermit2Payment(decoded) {
208
+ const payment = decoded;
209
+ return typeof payment?.payload?.permit2Authorization === "object" && payment.payload.permit2Authorization !== null && payment.accepted?.scheme === "upto";
210
+ }
211
+ function toFacilitatorPayload(decoded, network) {
212
+ if (!isStandardPermit2Payment(decoded)) return decoded;
213
+ const { signature, permit2Authorization: auth } = decoded.payload;
214
+ const permit = signedPermit(decoded.extensions?.eip2612GasSponsoring?.info);
215
+ return {
216
+ scheme: "upto",
217
+ network,
218
+ method: "uptoPermit2",
219
+ tokenIn: auth.permitted.token,
220
+ payload: {
221
+ signature,
222
+ authorization: {
223
+ from: auth.from,
224
+ to: auth.witness.to,
225
+ value: auth.permitted.amount,
226
+ validAfter: auth.witness.validAfter,
227
+ validBefore: auth.deadline,
228
+ nonce: paymentCore.permit2NonceToBytes32(auth.nonce)
229
+ },
230
+ ...permit && { permit2612: permit }
231
+ },
232
+ uptoParams: { payTo: auth.witness.to },
233
+ permit2Params: {
234
+ spender: auth.spender,
235
+ facilitator: auth.witness.facilitator
236
+ }
237
+ };
238
+ }
121
239
  var ethereumAddressSchema = zod.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid Ethereum address");
122
240
  var txHashSchema = zod.z.string().regex(/^0x[0-9a-fA-F]{64}$/);
123
241
  var priceSchema = zod.z.union([
@@ -152,6 +270,7 @@ var routeConfigSchema = zod.z.object({
152
270
  description: zod.z.string().optional(),
153
271
  scheme: zod.z.enum(["exact", "upto"]).optional(),
154
272
  maxTimeoutSeconds: zod.z.number().int().positive().optional(),
273
+ decimals: zod.z.number().int().nonnegative().optional(),
155
274
  meter: zod.z.custom(
156
275
  (value) => typeof value === "function",
157
276
  "meter must be a function of the response"
@@ -173,9 +292,12 @@ var routeConfigSchema = zod.z.object({
173
292
  const usdp = PARALLEL_TOKENS[route.network]?.usdp;
174
293
  if (!usdp) return true;
175
294
  return route.acceptedTokens.every(
176
- (token) => token.toLowerCase() === usdp.toLowerCase()
295
+ (token) => token.toLowerCase() === usdp.toLowerCase() || permit2Extra(route.network, token) !== void 0
177
296
  );
178
- }, "acceptedTokens on an upto route may only list the network's USDp");
297
+ }, "acceptedTokens on an upto route may only list the network's USDp, and its USDC where the escrow takes Permit2 payments").refine(
298
+ (route) => route.scheme !== "upto" || !hasMixedDecimals(route.network, route.acceptedTokens) || typeof route.price !== "bigint" && route.decimals === void 0,
299
+ 'An upto route that accepts tokens with different decimals needs a price like "0.01", not a bigint or an explicit decimals'
300
+ );
179
301
  var paymentMiddlewareConfigSchema = zod.z.object({
180
302
  facilitator: facilitatorConfigSchema,
181
303
  routes: zod.z.record(zod.z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
@@ -212,6 +334,9 @@ var settleSuccessSchema = zod.z.object({
212
334
  gasSponsored: zod.z.boolean(),
213
335
  // Metered routes: the amount actually charged (smallest unit).
214
336
  amountSettled: zod.z.string().optional(),
337
+ // A standard x402 Permit2 payment: the USDp the merchant received for
338
+ // amountSettled (smallest unit), when the facilitator read it.
339
+ amountDelivered: zod.z.string().optional(),
215
340
  // The facilitator answered from a recorded outcome, nothing was broadcast.
216
341
  alreadySettled: zod.z.boolean().optional()
217
342
  });
@@ -250,13 +375,17 @@ function throwIfRedirect(response) {
250
375
  status: response.status
251
376
  });
252
377
  }
253
- function decodePaymentHeader(paymentHeader) {
378
+ function decodePaymentHeader(paymentHeader, network) {
254
379
  try {
255
- return decodeBase64(paymentHeader);
380
+ return toFacilitatorPayload(decodeBase64(paymentHeader), network);
256
381
  } catch {
257
382
  throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.INVALID_PAYMENT);
258
383
  }
259
384
  }
385
+ function payerOf(paymentPayload) {
386
+ const from = paymentPayload?.payload?.authorization?.from;
387
+ return typeof from === "string" ? from : void 0;
388
+ }
260
389
  var FacilitatorClient = class {
261
390
  // Checked and kept when the client is built: a config changed afterwards
262
391
  // does not move the calls elsewhere.
@@ -274,7 +403,10 @@ var FacilitatorClient = class {
274
403
  this.apiKey = config.apiKey;
275
404
  }
276
405
  async verify(paymentHeader, paymentRequirements) {
277
- const paymentPayload = decodePaymentHeader(paymentHeader);
406
+ const paymentPayload = decodePaymentHeader(
407
+ paymentHeader,
408
+ paymentRequirements.network
409
+ );
278
410
  let json;
279
411
  try {
280
412
  const response = await fetch(`${this.url}/x402/verify`, {
@@ -309,7 +441,10 @@ var FacilitatorClient = class {
309
441
  }
310
442
  }
311
443
  async settle(paymentHeader, paymentRequirements, options) {
312
- const paymentPayload = decodePaymentHeader(paymentHeader);
444
+ const paymentPayload = decodePaymentHeader(
445
+ paymentHeader,
446
+ paymentRequirements.network
447
+ );
313
448
  let json;
314
449
  try {
315
450
  const response = await fetch(`${this.url}/x402/settle`, {
@@ -360,17 +495,27 @@ var FacilitatorClient = class {
360
495
  try {
361
496
  const parsed = settleResponseSchema.parse(json);
362
497
  if ("txHash" in parsed) {
498
+ const txHash = parsed.txHash;
499
+ const networkId = toEip155Network(paymentRequirements.network);
500
+ const payer = payerOf(paymentPayload);
363
501
  return {
364
502
  success: true,
365
- txHash: parsed.txHash,
503
+ txHash,
366
504
  route: parsed.route,
367
505
  chain: paymentRequirements.network,
368
506
  gasSponsored: parsed.gasSponsored,
369
- networkId: toEip155Network(paymentRequirements.network),
507
+ networkId,
370
508
  ...parsed.amountSettled !== void 0 && {
371
509
  amountSettled: parsed.amountSettled
372
510
  },
373
- ...parsed.alreadySettled && { alreadySettled: true }
511
+ ...parsed.amountDelivered !== void 0 && {
512
+ amountDelivered: parsed.amountDelivered
513
+ },
514
+ ...parsed.alreadySettled && { alreadySettled: true },
515
+ transaction: txHash,
516
+ network: networkId,
517
+ amount: parsed.amountSettled ?? paymentRequirements.maxAmountRequired,
518
+ ...payer && { payer }
374
519
  };
375
520
  }
376
521
  return {
@@ -449,7 +594,7 @@ async function requestContext(adapter) {
449
594
  body: adapter.getBody ? await adapter.getBody() : void 0
450
595
  };
451
596
  }
452
- async function resolvePrice(config, ctx) {
597
+ async function resolvePrice(config, ctx, tokens) {
453
598
  if (typeof config.price !== "function") return config.price;
454
599
  if (!ctx) throw new chunkKZZY5LS6_cjs.X402ConfigError("A price function needs the request");
455
600
  let price;
@@ -470,6 +615,11 @@ async function resolvePrice(config, ctx) {
470
615
  )} \u2014 expected a positive amount like "0.01" or a bigint`
471
616
  );
472
617
  }
618
+ if (typeof price === "bigint" && config.scheme === "upto" && hasMixedDecimals(config.network, tokens)) {
619
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
620
+ `The price function of route "${config.network}${ctx.path}" returned a bigint, but the route accepts tokens with different decimals: return a price like "0.01"`
621
+ );
622
+ }
473
623
  return price;
474
624
  }
475
625
  var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
@@ -521,6 +671,17 @@ function invalidSettleAmountResponse(reason) {
521
671
  reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE
522
672
  );
523
673
  }
674
+ function refusingBigints(meter) {
675
+ return async (ctx) => {
676
+ const amount = await meter(ctx);
677
+ if (typeof amount === "bigint") {
678
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
679
+ 'The meter returned a bigint, but the route accepts tokens with different decimals: return an amount like "0.75"'
680
+ );
681
+ }
682
+ return amount;
683
+ };
684
+ }
524
685
  var AMOUNT_FORMAT = /^\d+(?:\.\d+)?$/;
525
686
  async function resolveCharge(verified, settleHeader, response) {
526
687
  if (verified.meter) {
@@ -600,18 +761,27 @@ function priceInUnits(price, decimals, asset) {
600
761
  }
601
762
  function buildPaymentRequired(url, config, resolvedTokens, price) {
602
763
  const network = toEip155Network(config.network);
603
- const accepts = resolvedTokens.map((asset) => {
764
+ const scheme = config.scheme ?? "exact";
765
+ const entries = resolvedTokens.map((asset) => {
604
766
  const decimals = resolveDecimals(config, asset);
605
767
  return {
606
- scheme: config.scheme ?? "exact",
768
+ scheme,
607
769
  network,
608
770
  asset,
609
771
  amount: priceInUnits(price, decimals, asset),
610
772
  payTo: config.payTo,
611
773
  maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
612
- extra: { decimals }
774
+ extra: {
775
+ decimals,
776
+ ...scheme === "upto" && permit2Extra(config.network, asset)
777
+ }
613
778
  };
614
779
  });
780
+ const viaPermit2 = (entry) => "facilitatorAddress" in entry.extra;
781
+ const accepts = [
782
+ ...entries.filter(viaPermit2),
783
+ ...entries.filter((entry) => !viaPermit2(entry))
784
+ ];
615
785
  return {
616
786
  x402Version: 2,
617
787
  resource: {
@@ -619,13 +789,17 @@ function buildPaymentRequired(url, config, resolvedTokens, price) {
619
789
  description: config.description,
620
790
  mimeType: "application/json"
621
791
  },
622
- accepts
792
+ accepts,
793
+ ...accepts.some(viaPermit2) && {
794
+ extensions: EIP2612_GAS_SPONSORING_EXTENSION
795
+ }
623
796
  };
624
797
  }
625
- function decodePaymentPayload(header) {
798
+ function decodePaymentPayload(header, network) {
626
799
  try {
627
- const parsed = JSON.parse(
628
- Buffer.from(header, "base64").toString("utf8")
800
+ const parsed = toFacilitatorPayload(
801
+ JSON.parse(Buffer.from(header, "base64").toString("utf8")),
802
+ network
629
803
  );
630
804
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
631
805
  } catch {
@@ -644,7 +818,7 @@ function deliveredToken(payload, chainTokens) {
644
818
  return payload.tokenIn ?? chainTokens?.usdp;
645
819
  }
646
820
  function resolveDeliveredAsset(header, config, resolvedTokens) {
647
- const payload = decodePaymentPayload(header);
821
+ const payload = decodePaymentPayload(header, config.network);
648
822
  const target = payload ? deliveredToken(
649
823
  payload,
650
824
  PARALLEL_TOKENS[config.network]
@@ -719,7 +893,7 @@ function createPaymentGate(config) {
719
893
  let request = typeof routeConfig.price === "function" ? await requestContext(adapter) : void 0;
720
894
  let price;
721
895
  try {
722
- price = await resolvePrice(routeConfig, request);
896
+ price = await resolvePrice(routeConfig, request, resolvedTokens);
723
897
  } catch (err) {
724
898
  if (!chunkKZZY5LS6_cjs.isPriceRejection(err)) throw err;
725
899
  const status = Number.isInteger(err.status) && err.status >= 400 && err.status <= 599 ? err.status : 400;
@@ -770,7 +944,8 @@ function createPaymentGate(config) {
770
944
  )
771
945
  };
772
946
  }
773
- if (routeConfig.meter && !request) request = await requestContext(adapter);
947
+ const meter = routeConfig.meter && hasMixedDecimals(routeConfig.network, resolvedTokens) ? refusingBigints(routeConfig.meter) : routeConfig.meter;
948
+ if (meter && !request) request = await requestContext(adapter);
774
949
  return {
775
950
  type: "verified",
776
951
  scheme: routeConfig.scheme ?? "exact",
@@ -778,7 +953,7 @@ function createPaymentGate(config) {
778
953
  decimals: resolveDecimals(routeConfig, asset),
779
954
  handlerTimeoutMs: handlerTimeoutFor(routeConfig),
780
955
  settle: (options) => client.settle(paymentHeader, requirements, { ...options, settleToken }),
781
- ...routeConfig.meter && { meter: routeConfig.meter, request }
956
+ ...meter && { meter, request }
782
957
  };
783
958
  };
784
959
  }