@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 +57 -10
- package/dist/{chunk-TH77IB3T.cjs → chunk-MYCUOJ3H.cjs} +196 -21
- package/dist/{chunk-G7HSURPM.js → chunk-VSNDEQGS.js} +197 -22
- package/dist/express.cjs +5 -5
- package/dist/express.d.cts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/express.js +1 -1
- package/dist/fastify.cjs +11 -11
- package/dist/fastify.d.cts +1 -1
- package/dist/fastify.d.ts +1 -1
- package/dist/fastify.js +1 -1
- package/dist/hono.cjs +6 -6
- package/dist/hono.d.cts +1 -1
- package/dist/hono.d.ts +1 -1
- package/dist/hono.js +1 -1
- package/dist/index.cjs +15 -15
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/llm.d.cts +1 -1
- package/dist/llm.d.ts +1 -1
- package/dist/next.cjs +13 -13
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.js +1 -1
- package/dist/{types-BkGQ2lPA.d.cts → types-BWL9h1eY.d.cts} +11 -0
- package/dist/{types-BkGQ2lPA.d.ts → types-BWL9h1eY.d.ts} +11 -0
- package/package.json +5 -4
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,
|
|
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: {
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
503
|
+
txHash,
|
|
366
504
|
route: parsed.route,
|
|
367
505
|
chain: paymentRequirements.network,
|
|
368
506
|
gasSponsored: parsed.gasSponsored,
|
|
369
|
-
networkId
|
|
507
|
+
networkId,
|
|
370
508
|
...parsed.amountSettled !== void 0 && {
|
|
371
509
|
amountSettled: parsed.amountSettled
|
|
372
510
|
},
|
|
373
|
-
...parsed.
|
|
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
|
|
764
|
+
const scheme = config.scheme ?? "exact";
|
|
765
|
+
const entries = resolvedTokens.map((asset) => {
|
|
604
766
|
const decimals = resolveDecimals(config, asset);
|
|
605
767
|
return {
|
|
606
|
-
scheme
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
...
|
|
956
|
+
...meter && { meter, request }
|
|
782
957
|
};
|
|
783
958
|
};
|
|
784
959
|
}
|