diff --git a/DEFERRED_ARCHITECTURE.md b/DEFERRED_ARCHITECTURE.md new file mode 100644 index 0000000000..0a93deeb96 --- /dev/null +++ b/DEFERRED_ARCHITECTURE.md @@ -0,0 +1,404 @@ +# Deferred Scheme Architecture + +This document describes how the x402 deferred payment scheme is implemented using the SDK, covering the flow between client, server, and facilitator. + +--- + +## Overview + +The deferred scheme enables aggregated micropayments through signed vouchers. Instead of settling each payment on-chain, vouchers accumulate and are settled later in batches. + +**Lifecycle:** +1. Client requests access to a resource +2. Client retries with payment (signed voucher) +3. Later: Server initiates on-chain settlement + +--- + +## Step 1: Client Requests Access + +``` +Client Server Facilitator + │ │ │ + │ GET /resource │ │ + │ PAYER-IDENTIFIER: 0x123... │ │ + │ ─────────────────────────────► │ │ + │ │ │ + │ ┌─────┴─────┐ │ + │ │ deferred-scheme extension: │ + │ │ │ + │ │ 1. Extract buyer from header │ + │ │ 2. Fetch buyer data ─────────────────────►│ + │ │ GET /deferred/buyers/:buyer │ + │ │ ◄────────────────────────────────────────│ + │ │ 3. Return extension data │ + │ └─────┬─────┘ │ + │ │ │ + │ 402 Payment Required │ │ + │ { extensions: { │ │ + │ deferred-scheme: { │ │ + │ info: { voucher, account } │ │ + │ } │ │ + │ }} │ │ + │ ◄───────────────────────────── │ │ +``` + +### Client + +**Action:** Send initial request with `PAYER-IDENTIFIER` header. + +**SDK:** No changes needed. Client includes header in fetch request. + +```typescript +const response = await fetchWithPayment('https://api.example.com/resource', { + headers: { 'PAYER-IDENTIFIER': wallet.address } +}); +``` + +### Server + +**Action:** Return 402 with voucher state and account data. + +**SDK:** Uses the `deferred-scheme` ResourceServerExtension (mandatory for servers accepting deferred payments): + +```typescript +const deferredSchemeExtension: ResourceServerExtension = { + key: 'deferred-scheme', + + // Extract buyer from PAYER-IDENTIFIER header + enrichDeclaration: (declaration, transportContext) => { + const ctx = transportContext as HTTPRequestContext; + const buyer = ctx.adapter.getHeader('PAYER-IDENTIFIER'); + return { ...declaration, buyer }; + }, + + // Fetch voucher state and inject into 402 response + enrichPaymentRequiredResponse: async (declaration, context) => { + const buyer = declaration.buyer; + + if (!buyer) { + return { info: { type: 'new' }, schema: voucherSchema }; + } + + // Get voucherStorage mode from payment requirements + const voucherStorage = context.requirements[0]?.extra?.voucherStorage; + + // Fetch buyer data from facilitator (account + voucher if facilitator stores them) + const buyerData = await facilitator.getBuyerData(buyer, seller, asset, chainId); + + // Use local voucher if server stores, otherwise use facilitator's voucher + const voucher = voucherStorage === 'server' + ? await localVoucherStore.getLatestVoucher(buyer, seller, asset) + : buyerData.voucher; + + return { + info: { + type: voucher ? 'aggregation' : 'new', + voucher, + account: buyerData.account + }, + schema: voucherSchema, + }; + }, +}; +``` + +Note: Extension hooks can only populate `response.extensions`, not modify `accepts[]` (PaymentRequirements). This is why voucher data goes in `extensions['deferred-scheme']` rather than in `accepts[].extra`. + +### Facilitator + +**Action:** Provide on-chain account data and voucher state (if facilitator stores vouchers). + +**SDK:** `DeferredEvmScheme` exposes a custom endpoint for buyer data: + +``` +GET /deferred/buyers/:buyer?seller=0x...&asset=0x...&chainId=84532 + +Response: +{ + "account": { + "balance": "10000000", + "assetAllowance": "115792...", + "assetPermitNonce": "0", + "escrow": "0x..." + }, + "voucher": { + "id": "0x...", + "nonce": 5, + "valueAggregate": "5000000", + ... + }, + "signature": "0x..." +} +``` + +- `account` is always returned (on-chain escrow data) +- `voucher` and `signature` are returned if facilitator supports `deferred-voucher-store` extension (otherwise `null`) + +Facilitators advertise voucher storage capability via `/supported`: + +```json +{ + "kinds": [{ "scheme": "deferred", "network": "eip155:84532", ... }], + "extensions": ["deferred-voucher-store"] +} +``` + +--- + +## Step 2: Client Retries with Payment + +``` +Client Server Facilitator + │ │ │ + │ GET /resource │ │ + │ PAYMENT-SIGNATURE: │ │ + │ ─────────────────────────────► │ │ + │ │ │ + │ ┌─────┴─────┐ │ + │ │ onBeforeVerify │ + │ └─────┬─────┘ │ + │ │ │ + │ │ POST /verify │ + │ │ ────────────────────────────────► │ + │ │ ◄──────────────────────────────── │ + │ │ │ + │ ┌─────┴─────┐ │ + │ │ onAfterVerify │ + │ └─────┬─────┘ │ + │ │ │ + │ ┌─────┴─────┐ │ + │ │ onBeforeSettle │ + │ └─────┬─────┘ │ + │ │ │ + │ │ POST /settle (store voucher) │ + │ │ ────────────────────────────────► │ + │ │ ◄──────────────────────────────── │ + │ │ │ + │ ┌─────┴─────┐ │ + │ │ onAfterSettle │ + │ │ (store voucher locally if needed) │ + │ └─────┬─────┘ │ + │ │ │ + │ 200 OK + response │ │ + │ ◄───────────────────────────── │ │ +``` + +### Client + +**Action:** Create signed voucher and send payment. + +**SDK:** Uses `DeferredEvmScheme` client implementation for payment creation. The client: + +1. Reads payment requirements from `paymentRequired.accepts[]` (scheme, network, asset, amount) +2. Reads voucher state from `paymentRequired.extensions['deferred-scheme']` (current voucher, account balance) +3. Creates new voucher by incrementing nonce and adding payment amount to valueAggregate +4. Signs voucher using EIP-712 +5. Sends payment header with signed voucher + +### Server + +**Action:** Verify voucher, store it, grant access. + +**SDK:** Uses existing hooks: +- `onAfterSettle` - Store voucher if `requirements.extra.voucherStorage === "server"` + +### Facilitator + +**Action:** Verify voucher signature, check escrow balance, store voucher. + +**SDK:** `DeferredEvmScheme` implements standard `/verify` and `/settle` endpoints with deferred-specific logic. +- `/settle` stores voucher if `requirements.extra.voucherStorage === "facilitator"` + +--- + +## Step 3: On-Chain Settlement + +``` +Server Facilitator Blockchain + │ │ │ + │ POST /deferred/collect │ │ + │ { voucher, signature } │ │ + │ ─────────────────────────────► │ │ + │ │ collect(voucher, signature) │ + │ │ ────────────────────────────────► │ + │ │ ◄──────────────────────────────── │ + │ │ │ + │ { txHash } │ │ + │ ◄───────────────────────────── │ │ +``` + +### Server + +**Action:** Initiate settlement when ready (threshold, schedule, manual). + +**SDK:** Out-of-band operation. Server calls facilitator's custom endpoint directly, implemented by `DeferredEvmScheme` + +```typescript +// Server decides when to settle (not tied to x402 hooks) +async function settleVouchers() { + const vouchers = await voucherStore.getVouchersReadyForSettlement(); + for (const { voucher, signature } of vouchers) { + await facilitatorClient.collect({ voucher, signature }); + } +} +``` + +### Facilitator + +**Action:** Submit voucher to escrow contract on-chain. + +**SDK:** Custom endpoint for deferred settlement. + +``` +POST /deferred/collect +{ voucher, signature } + +Response: +{ txHash: "0x..." } +``` + +--- + +## Extensions + +### Server Extension: `deferred-scheme` + +The `deferred-scheme` extension is a **ResourceServerExtension** that servers must register to accept deferred payments. It handles: + +1. **`enrichDeclaration`**: Extracts buyer address from `PAYER-IDENTIFIER` header +2. **`enrichPaymentRequiredResponse`**: Fetches voucher state and injects into 402 response + +The extension uses both hooks internally - `enrichDeclaration` runs first for all extensions, then `enrichPaymentRequiredResponse` can access the enriched declaration. + +### Facilitator Extension: `deferred-voucher-store` + +The `deferred-voucher-store` extension is a **FacilitatorExtension** that facilitators can optionally support. It indicates the facilitator can store vouchers on behalf of servers. + +**Advertised in `/supported`:** +```json +{ + "kinds": [{ "scheme": "deferred", ... }], + "extensions": ["deferred-voucher-store"] +} +``` + +**What it enables:** +- `GET /deferred/buyers/:buyer` returns stored vouchers +- `POST /settle` stores vouchers when `voucherStorage === "facilitator"` + +Servers check this capability before configuring `voucherStorage: "facilitator"` in their payment requirements. + +--- + +## PaymentRequirements.extra + +The deferred scheme uses `extra` in `PaymentRequirements` for scheme-specific metadata: + +```typescript +extra: { + name: "USDC", // EIP-712 domain name + version: "2", // EIP-712 domain version + escrow: "0x...", // Escrow contract address + voucherStorage: "server" | "facilitator" // Who stores vouchers +} +``` + +The `voucherStorage` field declares where vouchers are stored. Both server and facilitator read this to determine their behavior. + +--- + +## Voucher Storage Modes + +Servers declare storage mode via `extra.voucherStorage` in payment requirements: + +``` +Server Facilitator + │ │ + │ Step 1: GET /buyers/:buyer │ (account + voucher if stored) + │ ─────────────────────────────► │ + │ ◄───────────────────────────── │ + │ │ + │ Step 2: POST /settle │ + │ ─────────────────────────────► │ + │ ◄───────────────────────────── │ +``` + +### Mode A: Server Stores (`voucherStorage: "server"`) + +- Server queries local database for latest voucher +- Server stores voucher in `onAfterSettle` hook +- Facilitator skips voucher storage in `/settle` + +**Pros:** Portable - can switch facilitators without losing voucher history +**Cons:** Server needs database infrastructure + +### Mode B: Facilitator Stores (`voucherStorage: "facilitator"`) + +- Server uses `voucher` from facilitator response +- Facilitator stores voucher as part of `/settle` +- Server skips local storage + +**Pros:** Simpler server implementation, no database needed +**Cons:** Tied to facilitator - switching means losing voucher history + +### How Each Actor Uses voucherStorage + +| Actor | Reads `voucherStorage` | Action | +|-------|------------------------|--------| +| Server (402 response) | `"server"` | Query local DB for voucher | +| Server (402 response) | `"facilitator"` | Use voucher from facilitator | +| Server (`onAfterSettle`) | `"server"` | Store voucher locally | +| Server (`onAfterSettle`) | `"facilitator"` | Skip local storage | +| Facilitator (`/settle`) | `"server"` | Skip voucher storage | +| Facilitator (`/settle`) | `"facilitator"` | Store voucher | + +--- + +## Extensions Summary + +| Extension | Type | Purpose | +|-----------|------|---------| +| `deferred-scheme` | ResourceServerExtension (mandatory) | Extract buyer header, fetch voucher state, inject into 402 | +| `deferred-voucher-store` | FacilitatorExtension (optional) | Voucher storage capability for facilitators | + +--- + +## Facilitator Custom Endpoints + +The deferred scheme requires facilitator endpoints beyond the standard x402 interface: + +| Endpoint | Purpose | +|----------|---------| +| `GET /buyers/:buyer` | Query on-chain account data + voucher (if facilitator stores vouchers) | +| `POST /deferred/collect` | Submit voucher for on-chain settlement | +| `POST /deferred/collectMany` | Batch settlement | +| `POST /deferred/deposit` | Execute gasless deposit (with permit) | + +When facilitator provides storage, vouchers are stored automatically as part of `/settle`. + +--- + +## Hooks Summary + +| Hook | When | Used By | +|------|------|---------| +| `enrichDeclaration` | Before building 402 | `deferred-scheme`: Extract buyer from header | +| `enrichPaymentRequiredResponse` | Building 402 response | `deferred-scheme`: Fetch and inject voucher state | +| `onAfterSettle` | After settlement | Server: Store voucher if `voucherStorage === "server"` | + +--- + +## Key Design Decisions + +1. **Payer identification via header**: Client sends `PAYER-IDENTIFIER` on initial request. No SDK changes needed - client just includes header in fetch. + +2. **Single server extension**: The `deferred-scheme` extension handles both header extraction and voucher state injection. Simpler than separate extensions. + +3. **Facilitator voucher storage as capability**: The `deferred-voucher-store` is advertised in facilitator's `/supported` response. Servers check this before configuring `voucherStorage: "facilitator"`. + +4. **Pluggable voucher storage**: Server chooses where to store vouchers via `extra.voucherStorage`. Both server and facilitator read this to determine behavior. + +5. **Out-of-band settlement**: On-chain settlement is not tied to HTTP request lifecycle. Server calls facilitator directly when ready. + +6. **Facilitator custom endpoints**: Deferred-specific operations (account data, collect) require endpoints beyond standard x402 facilitator interface. diff --git a/DEFERRED_ARCHITECTURE_V2.md b/DEFERRED_ARCHITECTURE_V2.md new file mode 100644 index 0000000000..304ae224dd --- /dev/null +++ b/DEFERRED_ARCHITECTURE_V2.md @@ -0,0 +1,707 @@ +# Deferred Scheme Architecture + +This document describes the x402 deferred payment scheme implementation, covering the flow between client (buyer), server (seller), facilitator, and blockchain. + +--- + +## Overview + +The deferred scheme enables aggregated micropayments through signed vouchers. Instead of settling each payment on-chain, vouchers accumulate and are settled later in batches. + +**Actors:** +- **Client (Buyer)**: Requests resources and signs vouchers +- **Server (Seller)**: Protects resources and holds voucher state +- **Facilitator**: Verifies payments, manages escrow interactions +- **Blockchain**: Escrow contract holding buyer deposits + +**Key Concepts:** +- `PAYER-IDENTIFIER` identifies the buyer, allowing the seller to consider voucher history when sending payment requirements +- Sellers hold voucher state, optionally delegating storage to facilitators +- Settling stores the voucher (server or facilitator), not on-chain +- On-chain settlement happens separately, triggered manually or periodically + +--- + +## Resource Request Flow + +``` +Client (Buyer) Server (Seller) Facilitator Blockchain + │ │ │ │ + │ 1. GET /api │ │ │ + │ PAYER-IDENTIFIER │ │ │ + │ ──────────────────► │ │ │ + │ │ 2a. /deferred/buyers/:buyer │ + │ │ ─────────────────────────► │ + │ │ │ 2b. Get on-chain balance + │ │ │ ─────────────────────►│ + │ │ │ 2c. Balance data │ + │ │ │ ◄─────────────────────│ + │ │ 2d. Buyer state │ │ + │ │ ◄───────────────────────── │ + │ │ │ │ + │ 3. 402 PAYMENT-REQUIRED │ │ + │ ◄────────────────── │ │ │ + │ │ │ │ + │ 4. GET /api │ │ │ + │ PAYMENT-SIGNATURE │ │ │ + │ (+ deposit auth) │ │ │ + │ ──────────────────► │ │ │ + │ │ 5a. POST /verify │ │ + │ │ ─────────────────────────► │ + │ │ │ 5b. Get on-chain state│ + │ │ │ ─────────────────────►│ + │ │ │ 5c. On-chain state │ + │ │ │ ◄─────────────────────│ + │ │ 5d. 200 Verification │ │ + │ │ ◄───────────────────────── │ + │ │ │ │ + │ ┌─────┴─────┐ │ │ + │ │ 6. Do work│ │ │ + │ └─────┬─────┘ │ │ + │ │ │ │ + │ │ 7a. POST /settle │ │ + │ │ ─────────────────────────► │ + │ │ │ 7b. Execute deposit │ + │ │ │ ─────────────────────►│ + │ │ │ 7c. Deposit confirmed │ + │ │ │ ◄─────────────────────│ + │ │ 7d. 200 Settled │ │ + │ │ ◄───────────────────────── │ + │ │ │ │ + │ 8. 200 OK │ │ │ + │ PAYMENT-RESPONSE │ │ │ + │ ◄────────────────── │ │ │ +``` + +--- + +## Step 1: Initial Request + +**Client → Server:** `GET /api` with optional `PAYER-IDENTIFIER` header + +### Client + +**Action:** Send request with optional buyer identifier header. + +**SDK:** No SDK changes needed. Client includes header in fetch request. + +```typescript +const response = await fetchWithPayment('https://api.example.com/resource', { + headers: { 'PAYER-IDENTIFIER': wallet.address } +}); +``` + +The `PAYER-IDENTIFIER` header is optional but recommended. It allows the server to look up existing voucher state before returning payment requirements, enabling voucher aggregation. + +### Server + +**Action:** Receive request, check for payment header, extract buyer identifier. + +**SDK:** Standard x402 middleware checks for payment header. If no payment, continues to build 402 response. The `deferred-scheme` extension extracts the buyer address from the header. + +--- + +## Steps 2a-2d: Buyer State Lookup + +**Server → Facilitator → Blockchain → Facilitator → Server** + +### Server (2a) + +**Action:** Query facilitator for buyer's escrow state and voucher history. + +**SDK:** Uses the `deferred-scheme` ResourceServerExtension: + +```typescript +const deferredSchemeExtension: ResourceServerExtension = { + key: 'deferred-scheme', + + // Extract buyer from PAYER-IDENTIFIER header + enrichDeclaration: (declaration, transportContext) => { + const ctx = transportContext as HTTPRequestContext; + const buyer = ctx.adapter.getHeader('PAYER-IDENTIFIER'); + return { ...declaration, buyer }; + }, + + // Fetch voucher state and inject into 402 response + enrichPaymentRequiredResponse: async (declaration, context) => { + const buyer = declaration.buyer; + + if (!buyer) { + return { info: { type: 'new' }, schema: voucherSchema }; + } + + // Get voucherStorage mode from payment requirements + const voucherStorage = context.requirements[0]?.extra?.voucherStorage; + + // Fetch buyer data from facilitator + const buyerData = await facilitator.getBuyerData(buyer, seller, asset, chainId); + + // Use local voucher if server stores, otherwise use facilitator's voucher + const voucher = voucherStorage === 'server' + ? await localVoucherStore.getLatestVoucher(buyer, seller, asset) + : buyerData.voucher; + + return { + info: { + type: voucher ? 'aggregation' : 'new', + voucher, + account: buyerData.account + }, + schema: voucherSchema, + }; + }, +}; +``` + +### Facilitator (2a → 2d) + +**Action:** Query blockchain for escrow balance, return buyer state. + +**SDK:** `DeferredEvmScheme` implements custom endpoint. + +``` +GET /deferred/buyers/:buyer?seller=0x...&asset=0x...&chainId=84532 + +Response: +{ + "account": { + "balance": "10000000", + "assetAllowance": "115792...", + "assetPermitNonce": "0", + "escrow": "0x..." + }, + "voucher": { + "id": "0x...", + "nonce": 5, + "valueAggregate": "5000000", + "buyer": "0x...", + "seller": "0x...", + "asset": "0x...", + "escrow": "0x...", + "timestamp": 1703123456, + "chainId": 84532 + }, + "signature": "0x..." +} +``` + +- `account` is always returned (on-chain escrow data from steps 2b-2c) +- `voucher` and `signature` are returned if facilitator supports `deferred-voucher-store` extension + +### Blockchain (2b-2c) + +**Action:** Return escrow contract state. + +**Contract calls:** +- `escrow.balanceOf(buyer, asset)` - Buyer's deposited balance +- `asset.allowance(buyer, escrow)` - Token allowance for deposits +- `asset.nonces(buyer)` - EIP-2612 permit nonce (if supported) + +--- + +## Step 3: Payment Required Response + +**Server → Client:** `402 PAYMENT-REQUIRED` + +### Server + +**Action:** Build and return payment requirements with voucher state. + +**SDK:** The `deferred-scheme` extension populates `response.extensions`, then server builds 402 response. + +```typescript +// Final 402 response structure: +{ + "x402Version": 2, + "accepts": [{ + "scheme": "deferred", + "network": "eip155:84532", + "maxAmountRequired": "1000000", + "resource": "https://api.example.com/resource", + "payTo": "0xSellerAddress", + "asset": "0xUSDCAddress", + "extra": { + "name": "USDC", + "version": "2", + "escrow": "0xEscrowAddress", + "voucherStorage": "server" + } + }], + "extensions": { + "deferred-scheme": { + "info": { + "type": "aggregation", + "voucher": { ... }, + "signature": "0x...", + "account": { + "balance": "10000000", + "assetAllowance": "115792...", + "assetPermitNonce": "0" + } + }, + "schema": { ... } + } + } +} +``` + +**Note:** Extension hooks can only populate `response.extensions`, not modify `accepts[]`. This is why voucher data goes in `extensions['deferred-scheme']` rather than in `accepts[].extra`. + +### Client + +**Action:** Receive 402, prepare payment. + +**SDK:** `wrapFetchWithPayment` intercepts 402 response. + +```typescript +// Client receives 402 and extracts: +// 1. Payment requirements from paymentRequired.accepts[] +// 2. Voucher state from paymentRequired.extensions['deferred-scheme'] +``` + +--- + +## Step 4: Payment Retry + +**Client → Server:** `GET /api` with `PAYMENT-SIGNATURE` header + +### Client + +**Action:** Create signed voucher and retry request. + +**SDK:** `DeferredEvmScheme` client implementation handles voucher creation. + +```typescript +// Client creates payment: +// 1. Read payment requirements from accepts[] (scheme, network, asset, amount) +// 2. Read voucher state from extensions['deferred-scheme'] +// 3. Create new voucher: +// - If type: "new" → nonce: 0, valueAggregate: paymentAmount +// - If type: "aggregation" → nonce: previous + 1, valueAggregate: previous + paymentAmount +// 4. Sign voucher using EIP-712 +// 5. Optionally include deposit authorization (permit signature) + +const paymentPayload = { + x402Version: 2, + scheme: "deferred", + network: "eip155:84532", + payload: { + voucher: { + id: "0x...", + nonce: 6, + valueAggregate: "6000000", + buyer: "0x...", + seller: "0x...", + asset: "0x...", + escrow: "0x...", + timestamp: 1703123500, + chainId: 84532 + }, + signature: "0x...", + // Optional: deposit authorization for gasless deposits + depositAuthorization: { + permit: { ... }, + permitSignature: "0x..." + } + } +}; +``` + +**Request:** +``` +GET /api +PAYMENT-SIGNATURE: base64(paymentPayload) +``` + +### Server + +**Action:** Receive payment, forward to facilitator for verification. + +**SDK:** Standard x402 flow - server automatically forwards payment to facilitator's `/verify` endpoint. + +--- + +## Steps 5a-5d: Verification + +**Server → Facilitator → Blockchain → Facilitator → Server** + +### Server (5a) + +**Action:** Forward payment to facilitator for verification. + +**SDK:** Standard x402 verification flow. + +``` +POST /verify +Content-Type: application/json + +{ + "paymentSignature": { ... }, + "paymentRequired": { ... } +} +``` + +### Facilitator (5a → 5d) + +**Action:** Verify voucher signature and check escrow balance. + +**SDK:** `DeferredEvmScheme` implements verification logic. + +**Verification steps:** +1. Decode voucher from payment payload +2. Verify EIP-712 signature matches buyer address +3. Validate voucher fields: + - `nonce == previousNonce + 1` (or `nonce == 0` for new voucher) + - `valueAggregate >= previousValueAggregate + paymentAmount` + - `seller`, `asset`, `escrow`, `chainId` match payment requirements +4. Query blockchain for escrow balance (steps 5b-5c) +5. Verify `balance >= valueAggregate` + +**Response:** +```json +{ + "valid": true, + "invalidReason": null +} +``` + +### Blockchain (5b-5c) + +**Action:** Return current escrow state for verification. + +**Contract calls:** +- `escrow.balanceOf(buyer, asset)` - Verify sufficient balance for voucher + +--- + +## Step 6: Do Work + +**Server:** Execute protected resource logic + +### Server + +**Action:** After successful verification, execute the protected resource handler. + +**SDK:** Application-specific logic runs here. + +```typescript +// Verification passed - execute protected handler +const result = await protectedHandler(request); +``` + +**Important:** Work is done BEFORE settlement. If settlement fails, the server has already done the work but may not store the voucher. Servers should handle this gracefully. + +--- + +## Steps 7a-7d: Settlement + +**Server → Facilitator → Blockchain → Facilitator → Server** + +### Server (7a) + +**Action:** Request settlement (store voucher, optionally execute deposit). + +**SDK:** Standard x402 settlement flow. + +``` +POST /settle +Content-Type: application/json + +{ + "paymentSignature": { ... }, + "paymentRequired": { ... } +} +``` + +### Facilitator (7a → 7d) + +**Action:** Store voucher, optionally execute deposit authorization. + +**SDK:** `DeferredEvmScheme` implements settlement logic. + +**Settlement steps:** +1. Store voucher if `requirements.extra.voucherStorage === "facilitator"` +2. If deposit authorization included: + - Execute `permit` call to approve escrow (step 7b) + - Execute `deposit` call to transfer tokens to escrow (step 7b) + - Wait for transaction confirmation (step 7c) + +**Response:** +```json +{ + "success": true, + "network": "eip155:84532", + "transaction": "0x...", // Only if deposit was executed + "payer": "0xBuyerAddress" +} +``` + +### Blockchain (7b-7c) + +**Action:** Execute deposit if authorization provided. + +**Contract calls (if deposit auth present):** +- `asset.permit(buyer, escrow, amount, deadline, v, r, s)` - Approve escrow +- `escrow.deposit(buyer, asset, amount)` - Transfer tokens to escrow + +### Server (post-7d) + +**Action:** Store voucher locally if using local storage mode. + +**SDK:** `onAfterSettle` hook. Stores voucher if `requirements.extra.voucherStorage === "server"`. + +```typescript +const serverHooks = { + onAfterSettle: async (result, context) => { + const voucherStorage = context.requirements.extra?.voucherStorage; + if (voucherStorage === 'server') { + await localVoucherStore.storeVoucher( + context.paymentPayload.payload.voucher, + context.paymentPayload.payload.signature + ); + } + } +}; +``` + +--- + +## Step 8: Success Response + +**Server → Client:** `200 OK` with `PAYMENT-RESPONSE` header + +### Server + +**Action:** Return protected resource with payment confirmation. + +**SDK:** Standard x402 response flow. + +``` +HTTP/1.1 200 OK +PAYMENT-RESPONSE: base64({ + "success": true, + "network": "eip155:84532", + "transaction": "0x...", + "payer": "0xBuyerAddress" +}) +Content-Type: application/json + +{ "data": "protected resource content" } +``` + +### Client + +**Action:** Receive response and payment confirmation. + +**SDK:** `wrapFetchWithPayment` returns the successful response to caller. + +--- + +## Payment Settlement (On-Chain) + +``` +Server (Seller) Facilitator Blockchain + │ │ │ + │ 1. POST /deferred/vouchers/settle │ + │ ─────────────────────────► │ + │ │ 2. Submit voucher │ + │ │ ─────────────────────►│ + │ │ 3. Tx confirmed │ + │ │ ◄─────────────────────│ + │ 4. Settled │ │ + │ ◄───────────────────────── │ +``` + +On-chain settlement is a separate flow, triggered manually or periodically by the server or facilitator. + +### Server (Step 1) + +**Action:** Initiate on-chain settlement when ready. + +**SDK:** Out-of-band operation. Server calls facilitator directly. + +```typescript +// Server decides when to settle (threshold, schedule, manual) +async function settleVouchersOnChain() { + const vouchers = await voucherStore.getVouchersReadyForSettlement(); + + for (const { voucher, signature } of vouchers) { + const result = await facilitatorClient.post('/deferred/vouchers/settle', { + voucher, + signature + }); + + if (result.success) { + await voucherStore.markAsSettled(voucher.id, result.txHash); + } + } +} +``` + +**Request:** +``` +POST /deferred/vouchers/settle +Content-Type: application/json + +{ + "voucher": { + "id": "0x...", + "nonce": 10, + "valueAggregate": "50000000", + "buyer": "0x...", + "seller": "0x...", + "asset": "0x...", + "escrow": "0x...", + "timestamp": 1703200000, + "chainId": 84532 + }, + "signature": "0x..." +} +``` + +### Facilitator (Steps 2-3) + +**Action:** Submit voucher to escrow contract. + +**SDK:** `DeferredEvmScheme` implements collection logic. + +**Contract call:** +```solidity +escrow.collect(voucher, signature) +// Transfers valueAggregate from buyer's escrow balance to seller +``` + +### Facilitator (Step 4) + +**Action:** Return settlement confirmation. + +**Response:** +```json +{ + "success": true, + "txHash": "0x...", + "blockNumber": 12345678 +} +``` + +--- + +## Extensions + +### Server Extension: `deferred-scheme` + +The `deferred-scheme` extension is a **ResourceServerExtension** that servers must register to accept deferred payments. It handles: + +1. **`enrichDeclaration`**: Extracts buyer address from `PAYER-IDENTIFIER` header +2. **`enrichPaymentRequiredResponse`**: Fetches voucher state and injects into 402 response + +The extension uses both hooks internally - `enrichDeclaration` runs first, then `enrichPaymentRequiredResponse` can access the enriched declaration with the buyer address. + +### Facilitator Extension: `deferred-voucher-store` + +The `deferred-voucher-store` extension is a **FacilitatorExtension** that facilitators can optionally support. It indicates the facilitator can store vouchers on behalf of servers. + +**Advertised in `/supported`:** +```json +{ + "kinds": [{ "scheme": "deferred", ... }], + "extensions": ["deferred-voucher-store"] +} +``` + +**What it enables:** +- `GET /deferred/buyers/:buyer` returns stored vouchers +- `POST /settle` stores vouchers when `voucherStorage === "facilitator"` + +Servers check this capability before configuring `voucherStorage: "facilitator"` in their payment requirements. + +--- + +## PaymentRequirements.extra + +The deferred scheme uses `extra` in `PaymentRequirements` for scheme-specific metadata: + +```typescript +extra: { + name: "USDC", // EIP-712 domain name + version: "2", // EIP-712 domain version + escrow: "0x...", // Escrow contract address + voucherStorage: "server" | "facilitator" // Who stores vouchers +} +``` + +The `voucherStorage` field declares where vouchers are stored. Both server and facilitator read this to determine their behavior. + +--- + +## Voucher Storage Modes + +Servers declare storage mode via `extra.voucherStorage` in payment requirements: + +### Mode A: Server Stores (`voucherStorage: "server"`) + +- Server queries local database for latest voucher +- Server stores voucher in `onAfterSettle` hook +- Facilitator skips voucher storage in `/settle` + +**Pros:** Portable - can switch facilitators without losing voucher history +**Cons:** Server needs database infrastructure + +### Mode B: Facilitator Stores (`voucherStorage: "facilitator"`) + +- Server uses `voucher` from facilitator response +- Facilitator stores voucher as part of `/settle` +- Server skips local storage + +**Pros:** Simpler server implementation, no database needed +**Cons:** Tied to facilitator - switching means losing voucher history + +### How Each Actor Uses voucherStorage + +| Actor | Reads `voucherStorage` | Action | +|-------|------------------------|--------| +| Server (402 response) | `"server"` | Query local DB for voucher | +| Server (402 response) | `"facilitator"` | Use voucher from facilitator | +| Server (`onAfterSettle`) | `"server"` | Store voucher locally | +| Server (`onAfterSettle`) | `"facilitator"` | Skip local storage | +| Facilitator (`/settle`) | `"server"` | Skip voucher storage | +| Facilitator (`/settle`) | `"facilitator"` | Store voucher | + +--- + +## Facilitator Custom Endpoints + +The deferred scheme requires facilitator endpoints beyond the standard x402 interface: + +| Endpoint | Purpose | +|----------|---------| +| `GET /deferred/buyers/:buyer` | Query on-chain account data + voucher (if facilitator stores vouchers) | +| `POST /deferred/vouchers/settle` | Submit voucher for on-chain settlement | +| `POST /deferred/vouchers/settleMany` | Batch on-chain settlement | + +--- + +## Extensions Summary + +| Extension | Type | Purpose | +|-----------|------|---------| +| `deferred-scheme` | ResourceServerExtension (mandatory) | Extract buyer header, fetch voucher state, inject into 402 | +| `deferred-voucher-store` | FacilitatorExtension (optional) | Voucher storage capability for facilitators | + +--- + +## Key Design Decisions + +1. **Payer identification via header**: Client sends `PAYER-IDENTIFIER` on initial request. Optional but enables voucher aggregation. + +2. **Single server extension**: The `deferred-scheme` extension handles both header extraction and voucher state injection. Simpler than separate extensions. + +3. **Facilitator voucher storage as capability**: The `deferred-voucher-store` is advertised in facilitator's `/supported` response. Servers check this before configuring `voucherStorage: "facilitator"`. + +4. **Pluggable voucher storage**: Server chooses where to store vouchers via `extra.voucherStorage`. Both server and facilitator read this to determine behavior. + +5. **Work before settlement**: Server executes protected handler after verification but before settlement. Settlement failure doesn't block response. + +6. **Separate on-chain settlement**: On-chain settlement is decoupled from request lifecycle. Triggered manually or periodically. + +7. **Optional deposit authorization**: Client can include permit signature for gasless deposits during settlement. diff --git a/specs/extensions/deferred-voucher-store.md b/specs/extensions/deferred-voucher-store.md new file mode 100644 index 0000000000..2a277f3791 --- /dev/null +++ b/specs/extensions/deferred-voucher-store.md @@ -0,0 +1,247 @@ +# Extension: `deferred-voucher-store` + +## Summary + +The `deferred-voucher-store` extension enables voucher storage and retrieval for the `deferred` payment scheme. It provides a pluggable interface that allows servers to choose where vouchers are stored - locally or delegated to a facilitator. + +This is a **Server ↔ Client** extension with optional **Facilitator** involvement for storage. + +**Key Design Principle:** The extension defines WHAT operations are needed (store/retrieve vouchers). The server chooses WHERE storage happens by selecting an appropriate backend implementation. This addresses facilitator portability concerns - servers can store vouchers locally and switch facilitators without losing voucher state. + +--- + +## PaymentRequired + +Server advertises support and current voucher state: + +```json +{ + "extensions": { + "deferred-voucher-store": { + "info": { + "type": "aggregation", + "voucher": { + "id": "0x...", + "nonce": 5, + "valueAggregate": "5000000", + "buyer": "0x...", + "seller": "0x...", + "asset": "0x...", + "escrow": "0x...", + "timestamp": 1703123456, + "chainId": 84532 + }, + "signature": "0x...", + "account": { + "balance": "10000000", + "assetAllowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "assetPermitNonce": "0" + } + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "voucher": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "nonce": { "type": "integer" }, + "valueAggregate": { "type": "string" }, + "buyer": { "type": "string" }, + "seller": { "type": "string" }, + "asset": { "type": "string" }, + "escrow": { "type": "string" }, + "timestamp": { "type": "integer" }, + "chainId": { "type": "integer" } + }, + "required": ["id", "nonce", "valueAggregate", "buyer", "seller", "asset", "escrow", "timestamp", "chainId"] + }, + "signature": { "type": "string" } + }, + "required": ["voucher", "signature"] + } + } + } +} +``` + +--- + +## `info` Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Response type: `"aggregation"` (existing voucher) or `"initial"` (no prior voucher) | +| `voucher` | object | No | Current voucher state (omitted when `type: "initial"`) | +| `signature` | string | No | Signature for current voucher (omitted when `type: "initial"`) | +| `account` | object | Yes | Buyer's escrow account information | + +### `type` Values + +- **`initial`**: No existing voucher for this buyer-seller-asset combination. Client creates a new voucher with `nonce: 0`. +- **`aggregation`**: Existing voucher found. Client increments `nonce` and adds payment amount to `valueAggregate`. + +### `voucher` Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique identifier for this buyer-seller pair (bytes32 hex) | +| `nonce` | integer | Current nonce (client increments by 1) | +| `valueAggregate` | string | Total accumulated value (client adds payment amount) | +| `buyer` | string | Buyer address | +| `seller` | string | Seller/payTo address | +| `asset` | string | ERC-20 token contract address | +| `escrow` | string | Escrow contract address | +| `timestamp` | integer | Unix timestamp of last aggregation | +| `chainId` | integer | Network chain ID | + +### `account` Fields + +| Field | Type | Description | +|-------|------|-------------| +| `balance` | string | Buyer's current escrow balance | +| `assetAllowance` | string | Token allowance for escrow contract | +| `assetPermitNonce` | string | Current EIP-2612 permit nonce | + +--- + +## PaymentPayload + +Client echoes the extension with updated voucher: + +```json +{ + "extensions": { + "deferred-voucher-store": { + "voucher": { + "id": "0x...", + "nonce": 6, + "valueAggregate": "6000000", + "buyer": "0x...", + "seller": "0x...", + "asset": "0x...", + "escrow": "0x...", + "timestamp": 1703123500, + "chainId": 84532 + }, + "signature": "0x..." + } + } +} +``` + +The client: +1. Increments `nonce` by 1 +2. Adds payment amount to `valueAggregate` +3. Updates `timestamp` to current time +4. Signs the voucher using EIP-712 + +--- + +## Server Behavior + +### On PaymentRequired + +1. Read buyer address from `payer-identifier` header (if available) +2. Query voucher store for existing voucher +3. Return `type: "initial"` or `type: "aggregation"` with current state +4. Include account balance information for client validation + +### On Payment Verification + +1. Validate voucher fields match expected values +2. Verify `nonce == previousNonce + 1` +3. Verify `valueAggregate >= previousValueAggregate + paymentAmount` +4. Forward to facilitator for signature verification and escrow balance check + +### On Settlement Success + +1. Store the new voucher and signature in the voucher store +2. This voucher becomes the baseline for the next aggregation + +--- + +## Facilitator Support + +Facilitators MAY advertise voucher storage capability: + +```json +// GET /supported +{ + "kinds": [...], + "extensions": ["deferred-voucher-store"] +} +``` + +When a facilitator supports this extension, servers MAY delegate storage operations to the facilitator instead of storing vouchers locally. + +--- + +## VoucherStore Interface + +Implementations MUST provide these operations: + +```typescript +interface VoucherStore { + // Store a new/aggregated voucher after successful settlement + storeVoucher(voucher: Voucher, signature: string): Promise; + + // Get latest voucher for aggregation (by buyer-seller-asset) + getLatestVoucher( + buyer: string, + seller: string, + asset: string + ): Promise<{ voucher: Voucher; signature: string } | null>; + + // Get account balance information + getAccountInfo( + buyer: string, + seller: string, + asset: string, + escrow: string, + chainId: number + ): Promise; +} +``` + +### Implementation Options + +**Option A: Server stores locally (portable)** + +```typescript +const extension = createDeferredVoucherStoreExtension({ + store: new ServerVoucherStore(database) +}); +``` + +**Option B: Server delegates to facilitator** + +```typescript +const extension = createDeferredVoucherStoreExtension({ + store: new FacilitatorVoucherStore(facilitatorClient) +}); +``` + +The server chooses the backend. The extension interface remains the same. + +--- + +## Security Considerations + +- **Voucher Signatures**: Vouchers are EIP-712 signed by the buyer. The facilitator verifies signatures before approving payments. +- **Nonce Ordering**: Strict `nonce == previousNonce + 1` prevents replay and ensures ordering. +- **Escrow Balance**: The facilitator checks that escrow balance covers `valueAggregate` before verification succeeds. +- **Storage Integrity**: If using local storage, servers MUST ensure voucher data is not corrupted or lost. Lost vouchers cannot be reconstructed. +- **Facilitator Portability**: Servers storing vouchers locally can switch facilitators without losing state. Servers delegating storage to a facilitator are dependent on that facilitator's availability. + +--- + +## Parallel Request Limitations + +The current nonce-based design does not support parallel requests well. Each payment must wait for the previous voucher to be stored before the next nonce can be used. + +Future versions may address this with: +- Nonce ranges (client reserves a range of nonces) +- Multiple voucher IDs per buyer-seller pair + diff --git a/specs/extensions/payer-identifier.md b/specs/extensions/payer-identifier.md new file mode 100644 index 0000000000..6df2e8e6e5 --- /dev/null +++ b/specs/extensions/payer-identifier.md @@ -0,0 +1,73 @@ +# Extension: `payer-identifier` + +## Summary + +The `payer-identifier` extension allows clients to provide **unauthenticated identification** to servers when initially requesting access to x402-protected resources. Servers can use this claimed identity to customize the payment requirements accordingly, saving one roundtrip between client and server. + +This is a **Server ↔ Client** extension. The Facilitator is not involved in the identification flow. + +**Important**: This extension provides identification, not authentication. The client may not control the claimed address. Servers must treat this as untrusted input and never grant access based solely on this header. For authenticated identification, use [sign-in-with-x](./sign-in-with-x.md). + +--- + +## PaymentRequired + +Server advertises support: + +```json +{ + "extensions": { + "payer-identifier": { + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 16, + "maxLength": 128, + "pattern": "^[a-zA-Z0-9_:-]+$" + } + } + } +} +``` + +The identifier is expected to be a string of 16-128 characters (alphanumeric, hyphens, underscores, colons). + +--- + +## Client Request + +The Client sends their identifier in the `PAYER-IDENTIFIER` HTTP header. + +```http +GET /weather HTTP/1.1 +Host: api.example.com +PAYER-IDENTIFIER: 0x857b06519E91e3A54538791bDbb0E22373e36b66 +``` + +--- + +## Server Behavior + +When the Server receives a request with the `PAYER-IDENTIFIER` header: + +1. **Parse**: Extract the identifier from the header +2. **Validate**: Verify the format matches expected length and characters +3. **Act**: Adjust response as required + +**Servers MAY:** +- Enrich `PaymentRequired` based on the claimed identity, using only publicly available data (pricing, available schemes, balances, payment history) +- Log the claimed identifier for correlation + +**Servers MUST NOT:** +- Grant access to protected resources based solely on this header +- Return off-chain private data based solely on this header +- Modify any state (balances, records) without a signed payment + +--- + +## Security Considerations + +- **Unauthenticated**: The header is an unauthenticated claim. Anyone can send any address. Servers MUST NOT grant access or modify state based solely on this header. +- **Public Data Only**: Servers should only use this header to look up publicly available data. Off-chain private data should not be returned based solely on this header. +- **Information Leakage**: Sending the header reveals the client's address before any payment. Clients should only send this header to trusted servers. + diff --git a/specs/schemes/deferred/scheme_deferred.md b/specs/schemes/deferred/scheme_deferred.md new file mode 100644 index 0000000000..f51692657d --- /dev/null +++ b/specs/schemes/deferred/scheme_deferred.md @@ -0,0 +1,13 @@ +# Scheme: `deferred` + +## Summary + +`deferred` is a scheme designed to support trust minimized micro-payments. Unlike the `exact` scheme, which requires a payment to be executed immediately and fully on-chain, `deferred` allows clients to issue signed vouchers (IOUs) off-chain, which can later be aggregated and redeemed by the seller. This scheme enables payments smaller than the minimum feasible on-chain transaction cost. + +`deferred` payment scheme requires the seller to store and manage the buyer's vouchers until their eventual on chain settlement. To simplify their setup sellers might choose to offload this task to trusted third parties providing these services, i.e facilitators. + +## Example Use Cases + +- AI agents or automated clients. +- Consuming an API requiring micro cent cost per request. +- Any case where payments are smaller than on-chain settlement costs. diff --git a/specs/schemes/deferred/scheme_deferred_evm.md b/specs/schemes/deferred/scheme_deferred_evm.md new file mode 100644 index 0000000000..03cb66cb96 --- /dev/null +++ b/specs/schemes/deferred/scheme_deferred_evm.md @@ -0,0 +1,348 @@ +# Scheme: `deferred` on `EVM` + +## Summary + +The `deferred` scheme on EVM chains uses `EIP-712` signed vouchers to represent payment commitments from a buyer to a seller. Before issuing vouchers, the buyer deposits funds—denominated in a specific `ERC-20` token—into an on-chain escrow earmarked for the seller. Each voucher authorizes a payment against that escrow balance, and explicitly specifies the asset being used. +Sellers can collect and aggregate these signed messages over time, choosing when to redeem them on-chain and settling the total amount in a single transaction. The funds in the escrow contract are subject to a thawing period when withdrawing, this gives sellers guarantee they will be able to redeem in time. +Interactions with the escrow contract for the buyer (depositing, thawing and withdrawing funds) are all performed via signed authorizations to remove the need for gas and blockchain access. These authorizations are executed and translated into on-chain actions by the facilitator. +This design enables efficient, asset-flexible micropayments without incurring prohibitive gas costs for every interaction. + + +## Protocol sequencing + +The deferred scheme follows the standard x402 flow with a key difference: payments are stored off-chain as signed vouchers during the main resource request flow but collected on-chain later in a deferred way. + + +### Resource Request Flow + +1. **Client** sends an HTTP request to a **resource server**, optionally including a `PAYER-IDENTIFIER` header to identify themselves. + +2. **Resource server** responds with `402 Payment Required`. The response includes scheme-specific static information (e.g., EIP-712 domain info) and, if the buyer identified themselves via `PAYER-IDENTIFIER`, buyer state including escrow balance and voucher history. + +3. **Client** creates a signed `voucher` based on the `PaymentRequirements` and embeds it into the `PaymentPayload`. The voucher can be an aggregation on top of a previous one, or a new one if there is no pre-existing history. Optionally, the client can include a signed `depositAuthorization` for gasless escrow top-up. + +4. **Client** sends the HTTP request with the `PAYMENT-SIGNATURE` header containing the `PaymentPayload`. + +5. **Resource server** verifies the `PaymentPayload` is valid either via local verification or by POSTing the `PaymentPayload` and `PaymentRequirements` to the `/verify` endpoint of a `facilitator`. + +6. **Facilitator** verifies the payload and voucher are valid and returns a `VerifyResponse`. + +7. If valid, **resource server** performs the work to fulfill the request. + +8. **Resource server** settles the payment either locally or by POSTing the `PaymentPayload` and `PaymentRequirements` to the `/settle` endpoint of a `facilitator`. Note that the payment is not actually collected at this stage—the voucher is stored locally or at the facilitator for deferred on-chain collection. If the `PaymentPayload` included a `depositAuthorization`, it is executed on-chain at this point. + +9. **Resource server** returns `200 OK` with the resource and a `PAYMENT-RESPONSE` header. + +### On-Chain Settlement Flow + +On-chain settlement is decoupled from the request flow and triggered at the seller's or facilitator's discretion: + +1. **Resource server or Facilitator** decides to settle vouchers based on a threshold, schedule, or manual trigger. + +2. **Resource server or Facilitator** collects by interacting with the escrow contract on the blockchain. This transfers payment from the buyer's escrow balance to the seller. + + +## `PAYMENT-SIGNATURE` header payload + +The `payload` field of the `X-PAYMENT` header must contain the following fields: + +- `signature`: The signature of the `EIP-712` voucher. +- `voucher`: parameters required to reconstruct the signed message for the operation. +- `depositAuthorization` (optional): A signed authorization allowing the facilitator to deposit funds into escrow on behalf of the buyer. This enables gasless deposits for new buyers or when additional funds are needed. + +### Voucher Fields + +- `id`: Unique identifier for the voucher (bytes32) +- `buyer`: Address of the payment initiator (address) +- `seller`: Address of the payment recipient (address) +- `valueAggregate`: Total outstanding amount in the voucher, monotonically increasing (uint256) +- `asset`: ERC-20 token address (address) +- `timestamp`: Last aggregation timestamp (uint64) +- `nonce`: Incremented with each aggregation (uint256) +- `escrow`: Address of the escrow contract (address) +- `chainId`: Network chain ID (uint256) + +Example: + +```json +{ + "signature": "0x3a2f7e3b6c1d8e9c0f64f8724e5cfb8bfe9a3cdb1ad6e4a876f7d418e47e96b11a23346a1b0e60c8d3a4c4fd0150a244ab4b0e6d6c5fa4103f8fa8fd2870a3c81b", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "2000000000000000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673000, + "nonce": 3, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + } +} +``` + +Full `X-PAYMENT` header (without deposit authorization): + +```json +{ + "x402Version": 1, + "scheme": "deferred", + "network": "base-sepolia", + "payload": { + "signature": "0x3a2f7e3b6c1d8e9c0f64f8724e5cfb8bfe9a3cdb1ad6e4a876f7d418e47e96b11a23346a1b0e60c8d3a4c4fd0150a244ab4b0e6d6c5fa4103f8fa8fd2870a3c81b", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "2000000000000000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673000, + "nonce": 3, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + } + } +} +``` + +### Deposit Authorization Fields (optional) + +The `depositAuthorization` object enables gasless escrow deposits by allowing the facilitator to execute deposits on behalf of the buyer. This is particularly useful for first-time buyers or when escrow balance needs to be topped up. Note that only assets implementing ERC-2612 permit extension are supported for gasless deposits. + +The structure consists of two parts: + +**Required:** +- `depositAuthorization`: EIP-712 signed authorization for the escrow contract + - `buyer`: Address of the buyer authorizing the deposit (address) + - `seller`: Address of the seller receiving the escrow deposit (address) + - `asset`: ERC-20 token contract address (address) + - `amount`: Amount to deposit in atomic token units (uint256) + - `nonce`: Unique bytes32 for replay protection (bytes32) + - `expiry`: Authorization expiration timestamp (uint64) + - `signature`: EIP-712 signature of the deposit authorization (bytes) + +**Optional:** +- `permit`: EIP-2612 permit for the ERC-20 token + - `owner`: Token owner address (address) + - `spender`: Escrow contract address (address) + - `value`: Token amount to approve (uint256) + - `nonce`: Token contract nonce for the permit (uint256/bigint) + - `deadline`: Permit expiration timestamp (uint256) + - `domain`: Token's EIP-712 domain + - `name`: Token name (string) + - `version`: Token version (string) + - `signature`: EIP-2612 signature of the permit (bytes) + +Example `X-PAYMENT` header with deposit authorization: + +```json +{ + "x402Version": 1, + "scheme": "deferred", + "network": "base-sepolia", + "payload": { + "signature": "0x3a2f7e3b6c1d8e9c0f64f8724e5cfb8bfe9a3cdb1ad6e4a876f7d418e47e96b11a23346a1b0e60c8d3a4c4fd0150a244ab4b0e6d6c5fa4103f8fa8fd2870a3c81b", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "2000000000000000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673000, + "nonce": 3, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + }, + "depositAuthorization": { + "permit": { + "owner": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "spender": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "value": "5000000", + "nonce": "0", + "deadline": 1740759400, + "domain": { + "name": "USD Coin", + "version": "2" + }, + "signature": "0x8f9e2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f1b" + }, + "depositAuthorization": { + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "amount": "5000000", + "nonce": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "expiry": 1740759400, + "signature": "0xbfdc3d0ae7663255972fdf5ce6dfc7556a5ac1da6768e4f4a942a2fa885737db5ddcb7385de4f4b6d483b97beb6a6103b46971f63905a063deb7b0cfc33473411b" + } + } + } +} +``` + +## `paymentRequirements` extra object + +The `extra` object in the "Payment Required Response" should contain the following fields: + +### Common Fields +- `type`: Indicates whether this is a `"new"` voucher or an `"aggregation"` of an existing voucher +- `account` (optional): Current escrow account details for the buyer-seller-asset tuple + - `balance`: Current escrow balance in atomic token units + - `assetAllowance`: Current token allowance for the escrow contract + - `assetPermitNonce`: Current permit nonce for the token contract + - `assetDomainName`: EIP-712 domain name for the asset + - `assetDomainVersion`: EIP-712 domain version for the asset + - `facilitator`: Address of the facilitator managing the escrow + +### For New Vouchers (`type: "new"`) +- `voucher`: A simplified voucher object containing: + - `id`: The voucher id to use for the new voucher (bytes32) + - `escrow`: The address of the escrow contract (address) + +### For Aggregation (`type: "aggregation"`) +- `signature`: The signature of the latest voucher corresponding to the given `id` (bytes) +- `voucher`: The complete latest voucher corresponding to the given `id` (all voucher fields) + +### Examples + +**New voucher (without account details):** + +```json +{ + "extra": { + "type": "new", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27" + } + } +} +``` + +**Aggregation (with account details):** + +```json +{ + "extra": { + "type": "aggregation", + "account": { + "balance": "5000000", + "assetAllowance": "5000000", + "assetPermitNonce": "0", + "assetDomainName": "USDC", + "assetDomainVersion": "2", + "facilitator": "https://facilitator.com" + }, + "signature": "0x3a2f7e3b6c1d8e9c0f64f8724e5cfb8bfe9a3cdb1ad6e4a876f7d418e47e96b11a23346a1b0e60c8d3a4c4fd0150a244ab4b0e6d6c5fa4103f8fa8fd2870a3c81b", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "2000000000000000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673000, + "nonce": 3, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + } + } +} +``` + +## Verification + +The following steps are required to verify a deferred payment: + +1. **Signature validation**: Verify the EIP-712 signature is valid +2. **Payment requirements matching**: + - Verify scheme is `"deferred"` + - Verify `paymentPayload.network` matches `paymentRequirements.network` + - Verify `paymentRequirements.payTo` matches `paymentPayload.voucher.seller` + - Verify `paymentPayload.voucher.asset` matches `paymentRequirements.asset` + - Verify `paymentPayload.voucher.chainId` matches the chain specified by `paymentRequirements.network` +3. **Voucher aggregation validation** (if aggregating an existing voucher): + - Verify `nonce` equals the previous `nonce + 1` + - Verify `valueAggregate` is equal to the previous `valueAggregate + paymentRequirements.maxAmountRequired` + - Verify `timestamp` is greater than the previous `timestamp` + - Verify `buyer`, `seller`, `asset`, `escrow` and `chainId` all match the previous voucher values +4. **Amount validation**: + - Verify `paymentPayload.voucher.valueAggregate` is enough to cover `paymentRequirements.maxAmountRequired` plus previous voucher value aggregate if it's an aggregate voucher +5. **Escrow balance check**: + - Verify the `buyer` has enough of the `asset` (ERC20 token) in the escrow to cover the valueAggregate in the `payload.voucher` + - Verify `id` has not been already collected in the escrow, or if it has, that the new balance is greater than what was already paid (in which case the difference will be paid) +6. **Deposit authorization validation** (if present): + - Verify the `depositAuthorization.depositAuthorization.signature` is a valid EIP-712 signature + - Verify `depositAuthorization.depositAuthorization.buyer` matches `paymentPayload.voucher.buyer` + - Verify `depositAuthorization.depositAuthorization.seller` matches `paymentPayload.voucher.seller` + - Verify `depositAuthorization.depositAuthorization.asset` matches `paymentPayload.voucher.asset` + - Verify `depositAuthorization.depositAuthorization.expiry` has not passed + - Verify the nonce has not been used before by checking the escrow contract + - If `permit` is present: + - Verify the `permit.signature` is a valid EIP-2612 signature + - Verify the permit nonce is valid by checking the token contract + - Verify `permit.owner` matches the buyer + - Verify `permit.spender` matches the escrow contract address + - Verify `permit.value` is sufficient to cover the deposit amount + - Verify `permit.deadline` has not passed +7. **Transaction simulation** (optional but recommended): + - Simulate the voucher collection to ensure the transaction would succeed on-chain + +## Deposit Authorization Execution + +When a `depositAuthorization` is included in the payment payload, the facilitator should execute it before storing the voucher. This ensures that the buyer has sufficient funds escrowed before the voucher is stored, preventing invalid vouchers from being accepted. + +## Settlement + +Settlement is performed via the facilitator calling the `collect` function on the escrow contract with the `payload.signature` and `payload.voucher` parameters from the `X-PAYMENT` header. This can be initiated by buyer's request or the facilitator holding the vouchers could trigger automatic settlement based on pre-agreed conditions. + +Multiple vouchers may be collected in a single transaction, using the `collectMany` function. + +## Appendix + +### `X-Payment-Buyer` header + +The `X-PAYMENT-BUYER` header allows buyers to notify sellers about their identity before signing any voucher or message. This enables sellers to determine whether to request a new voucher or check their voucher store for existing vouchers for further aggregation. It's important to note this header requires no proof of identity, the seller assumes the buyer is who it claims to be. This is not a problem however since the payment flow will later require valid signatures which an impostor wont be able to forge. + +The header contains the buyer's EVM address as a simple string: + +``` +X-PAYMENT-BUYER: 0x209693Bc6afc0C5328bA36FaF03C514EF312287C +``` + +The buyer needs to add this header when initially requesting access to a resource. Failing to provide the header will result in new vouchers being created on each interaction, defeating the purpose of the `deferred` scheme. + +Example 402 response with an existing voucher: +```json +{ + "x402Version": 1, + "accepts": [{ + "scheme": "deferred", + "network": "base-sepolia", + "maxAmountRequired": "1000000", + "payTo": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "extra": { + "type": "aggregation", + "signature": "0x3a2f7e3b...", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "5000000", + "nonce": 2, + // ... other voucher fields + } + } + }] +} +``` + +### Facilitator specification + +Facilitators supporting the `deferred` scheme should implement a voucher store for sellers and new APIs. Specification for these can be found here: +- [Voucher Store specification](./voucher_store.md) +- [Deferred Facilitator specification](./scheme_deferred_evm_facilitator.md) + +### Escrow contract specification + +The full specification for the deferred escrow contract can be found here: [DeferredPaymentEscrow specification](./scheme_deferred_evm_escrow_contract.md) \ No newline at end of file diff --git a/specs/schemes/deferred/scheme_deferred_evm_escrow_contract.md b/specs/schemes/deferred/scheme_deferred_evm_escrow_contract.md new file mode 100644 index 0000000000..ef81267705 --- /dev/null +++ b/specs/schemes/deferred/scheme_deferred_evm_escrow_contract.md @@ -0,0 +1,227 @@ + +# DeferredPaymentEscrow Contract Specification + +## Summary + +The `DeferredPaymentEscrow` contract enables micropayments using an escrow-based voucher system. Buyers deposit ERC-20 tokens into escrow accounts for specific sellers, then issue off-chain EIP-712 signed vouchers that sellers can redeem against those deposits. This approach allows for efficient aggregation of many small payments before on-chain settlement, while maintaining security through cryptographic signatures and time-bounded withdrawals. + +The contract is designed for scenarios where payments are frequent but small (micropayments), making individual on-chain transactions economically inefficient. It provides strong guarantees to both parties: buyers retain control over their deposited funds through a thawing mechanism, while sellers can collect payments immediately when vouchers are presented. + +## Contract Overview + +The contract manages deposits, withdrawals, and voucher redemption: + +- **Deposits**: Buyers deposit ERC-20 tokens for specific sellers +- **Vouchers**: Off-chain signed promises to pay that aggregate over time +- **Collection**: Sellers redeem vouchers against escrow balances +- **Withdrawal**: Buyers can withdraw unused funds after a thawing period +- **Authorizations**: EIP-712 signed operations for gasless interactions (designed for x402 Facilitators to be able to abstract escrow management actions from buyers) + +## Data Structures + +### EscrowAccount +```solidity +struct EscrowAccount { + uint256 balance; // Total deposited balance (includes thawing amount) + uint256 thawingAmount; // Amount currently thawing for withdrawal (subset of balance) + uint64 thawEndTime; // When thawing completes +} +``` + +**Important**: The `balance` field represents the total amount of tokens held in the escrow account, which includes any amount currently thawing. The `thawingAmount` is a subset of the `balance` that has been marked for withdrawal after the thawing period. Available funds for new thawing operations = `balance - thawingAmount`. + +### Voucher +```solidity +struct Voucher { + bytes32 id; // Unique identifier per buyer-seller pair + address buyer; // Payment initiator + address seller; // Payment recipient + uint256 valueAggregate; // Total accumulated amount (monotonically increasing) + address asset; // ERC-20 token address + uint64 timestamp; // Last aggregation timestamp + uint256 nonce; // Incremented with each aggregation + address escrow; // This contract's address + uint256 chainId; // Network chain ID +} +``` + +## Account Structure + +The contract uses a triple-nested mapping to organize escrow accounts: +``` +buyer → seller → asset → EscrowAccount +``` + +This structure ensures: +- Each buyer-seller pair has independent accounts +- Different assets (tokens) are tracked separately +- Clean separation of concerns between relationships + +## Payment Flow + +### 1. Deposit Phase +``` +Buyer → deposit(seller, asset, amount) → Escrow Contract + +OR + +Buyer → depositWithAuthorization(auth, signature) → Escrow Contract +``` + +### 2. Service & Voucher Phase +``` +Buyer ↔ Seller (off-chain interactions) +Buyer → signs Voucher(id, valueAggregate, ...) → Seller +``` + +### 3. Collection Phase +``` +Seller → collect(voucher, signature) → Escrow Contract +Escrow Contract → transfer(asset, amount) → Seller +``` + +### 4. Withdrawal Phase (if needed) +``` +Buyer → thaw(seller, asset, amount) → Escrow Contract +[wait THAWING_PERIOD] +Buyer → withdraw(seller, asset) → Escrow Contract + +OR + +Buyer → flushWithAuthorization(auth) → Escrow Contract +``` + +## Verification + +To verify a payment in the `deferred` scheme: + +1. **Signature Validation**: Verify the voucher signature using EIP-712 and ERC-1271 +2. **Contract Verification**: Ensure `voucher.escrow` matches the expected contract address +3. **Chain Verification**: Ensure `voucher.chainId` matches the current network +4. **Balance Check**: Verify escrow account has sufficient balance for collection +5. **Aggregation Validation**: Ensure `voucher.valueAggregate >= previous_collections` + +## Settlement + +Settlement occurs when sellers call the `collect` function: + +1. **Validation**: Contract validates voucher parameters and signature +2. **Amount Calculation**: Determines collectable amount based on: + - Total voucher value (`valueAggregate`) + - Previously collected amounts for this voucher ID + - Available balance in escrow account +3. **State Updates**: Records new collected amount and updates escrow balance +4. **Transfer**: Sends tokens directly to seller +5. **Events**: Emits collection events for off-chain tracking + +### Partial Collection + +If escrow balance is insufficient for the full voucher amount: +- Contract collects only the available amount +- Voucher remains valid for future collection of remaining amount +- Prevents voucher failures due to temporary fund shortages + +**Note for Sellers**: Before accepting a voucher off-chain, sellers should verify that the escrow account has sufficient balance to cover the voucher amount. This can be checked using `getOutstandingAndCollectableAmount(voucher)` which returns both the outstanding amount owed and the amount that can actually be collected immediately. + +## Withdrawal Protection + +The thawing mechanism protects sellers from sudden fund withdrawals: + +1. **Thaw Initiation**: Buyer calls `thaw(seller, asset, amount)` (calling `thaw()` multiple times will add to the thawing amount and reset the timer) +2. **Thawing Period**: Set at contract deployment (standard value is 1 day, though other escrow instances can be deployed with different thawing periods if needed) +3. **Seller Collection**: Sellers can still collect from full balance during thawing +4. **Withdrawal**: After thawing period, buyer can withdraw thawed amount +5. **Cancellation**: Buyers can cancel thawing at any time before completion + +## Authorization System + +### Gasless Operations + +The contract supports EIP-712 signed authorizations for gasless operations, designed for x402: + +### Deposit Authorization +Allows x402 Facilitators to execute deposits on behalf of buyers: +```solidity +struct DepositAuthorization { + address buyer; // Who is authorizing + address seller; // Recipient + address asset; // Token to deposit + uint256 amount; // Amount to deposit + bytes32 nonce; // Random bytes32 for replay protection + uint64 expiry; // Authorization expiration +} +``` + +### Flush Authorization +Enables x402 Facilitators to "flush" funds for buyers (withdraws any funds that have completed thawing, then starts thawing any remaining balance): +```solidity +struct FlushAuthorization { + address buyer; // Who is authorizing + address seller; // Specific account to flush + address asset; // Specific asset to flush + bytes32 nonce; // Random bytes32 for replay protection + uint64 expiry; // Authorization expiration +} +``` + +### Flush All Authorization +Allows batch withdrawal from all of a buyer's escrow accounts (performs flush operation on every account): +```solidity +struct FlushAllAuthorization { + address buyer; // Who is authorizing + bytes32 nonce; // Random bytes32 for replay protection + uint64 expiry; // Authorization expiration +} +``` + +## Contract Interface + +### Core Functions + +### Deposits +- `deposit(address seller, address asset, uint256 amount)` - Direct deposit +- `depositTo(address buyer, address seller, address asset, uint256 amount)` - Third-party deposit +- `depositMany(address asset, DepositInput[] deposits)` - Batch deposits +- `depositWithAuthorization(DepositAuthorization auth, bytes signature)` - Gasless deposit + +### Withdrawals +- `thaw(address seller, address asset, uint256 amount)` - Initiate withdrawal +- `cancelThaw(address seller, address asset)` - Cancel ongoing thaw +- `withdraw(address seller, address asset)` - Complete withdrawal +- `flushWithAuthorization(FlushAuthorization auth, bytes signature)` - Gasless specific flush +- `flushAllWithAuthorization(FlushAllAuthorization auth, bytes signature)` - Gasless batch flush + +### Collections +- `collect(Voucher voucher, bytes signature)` - Single voucher redemption +- `collectMany(SignedVoucher[] vouchers)` - Batch voucher redemption + +### View Functions +- `getAccount(address buyer, address seller, address asset)` → `EscrowAccount` - Get escrow account details +- `getAccountDetails(address buyer, address seller, address asset, bytes32[] voucherIds, uint256[] valueAggregates)` → `(uint256 balance, uint256 allowance, uint256 nonce)` - Get account details including available balance after accounting for pending vouchers, token allowance, and permit nonce. Returns: + - `balance`: Available escrow balance minus thawing amount and minus amounts needed for the provided voucher collections + - `allowance`: Current token allowance granted to the escrow contract + - `nonce`: Current EIP-2612 permit nonce for the buyer on the asset token contract +- `getVoucherCollected(address buyer, address seller, address asset, bytes32 voucherId)` → `uint256` - Get total amount already collected for this voucher ID +- `getOutstandingAndCollectableAmount(Voucher voucher)` → `(uint256 outstanding, uint256 collectable)` - Returns outstanding amount still owed and amount that can be collected immediately given current escrow balance +- `isVoucherSignatureValid(Voucher voucher, bytes signature)` → `bool` - Validate voucher signature +- `isDepositAuthorizationValid(DepositAuthorization auth, bytes signature)` → `bool` - Validate deposit authorization signature +- `isFlushAuthorizationValid(FlushAuthorization auth, bytes signature)` → `bool` - Validate flush authorization signature +- `isFlushAllAuthorizationValid(FlushAllAuthorization auth, bytes signature)` → `bool` - Validate flush all authorization signature + +### Constants +- `THAWING_PERIOD()` → `uint256` - Withdrawal thawing period (immutable, set at deployment) +- `MAX_THAWING_PERIOD()` → `uint256` - Maximum allowed thawing period (30 days) +- `DOMAIN_SEPARATOR()` → `bytes32` - EIP-712 domain separator + +## Appendix + +### Multi-Chain Deployment + +While each contract instance operates on a single chain, the design supports multi-chain deployments: +- Vouchers include `chainId` for chain-specific validation +- Contract will be deployed using Safe Singleton Factory for deterministic addresses across chains +- Cross-chain coordination must be handled at the application layer + +### Reference Implementation + +A reference implementation for this contract is provided with this repository, it can be found at [DeferredPaymentEscrow]() [TODO: update this when the implementation is ready] \ No newline at end of file diff --git a/specs/schemes/deferred/scheme_deferred_evm_facilitator.md b/specs/schemes/deferred/scheme_deferred_evm_facilitator.md new file mode 100644 index 0000000000..afaba0f0de --- /dev/null +++ b/specs/schemes/deferred/scheme_deferred_evm_facilitator.md @@ -0,0 +1,440 @@ +# Deferred Facilitator Specification + +## Summary + +This specification defines the REST API endpoints that facilitators must implement to support the `deferred` payment scheme. These endpoints enable sellers to store, retrieve, and settle vouchers through the facilitator's voucher store infrastructure. Vouchers are identified by a unique combination of `id` (64-character hex string) and `nonce` (non-negative integer). + +All endpoints are served under the facilitator's deferred scheme namespace: `${FACILITATOR_URL}/deferred/` + +## Authentication + +Read only endpoints do not require any form of authentication. Any information that can be retrieved by these endpoints will eventually be publicly available on chain. +As for write endpoints, they do not require traditional authentication but instead they rely on verification of signed messages. See each endpoint for details. + +## Required APIs + +### GET /buyers/:buyer + +Retrieves buyer data for a specific buyer, including escrow account balance, asset allowance, permit nonce, and the latest available voucher for a particular seller and asset. + +**Query Parameters:** +- `seller` (required): Seller address (e.g., "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D") +- `asset` (required): Asset address (e.g., "0x081827b8c3aa05287b5aa2bc3051fbe638f33152") +- `escrow` (required): Escrow address (e.g., "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27") +- `chainId` (required): Chain ID (e.g., 84532) + +**Example Request:** +``` +GET /buyers/0x209693Bc6afc0C5328bA36FaF03C514EF312287C?seller=0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D&asset=0x081827b8c3aa05287b5aa2bc3051fbe638f33152&escrow=0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27&chainId=84532 +``` + +**Response (200 OK):** +```json +{ + "balance": "10000000", + "assetAllowance": "5000000", + "assetPermitNonce": "0", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "5000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673000, + "nonce": 2, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + "signature": "0x3a2f7e3b..." + } +} +``` + +**Response (200 OK - No voucher available):** +```json +{ + "balance": "10000000", + "assetAllowance": "5000000", + "assetPermitNonce": "0" +} +``` + +**Response (400 Bad Request):** +```json +{ + "error": "Invalid parameters" +} +``` + +### POST /vouchers + +Stores a new signed voucher in the facilitator's voucher store after verifying it. The verification should be exactly the same as you'd get by POSTing to /verify. This allows for replacing that call for one to this endpoint. + +If the payment payload contains a `depositAuthorization`, the facilitator must execute it **before** storing the voucher: +1. If a `permit` is present, call the token contract's `permit` function +2. Call the escrow contract's `depositWithAuthorization` function +3. Verify the deposit succeeded by checking the escrow balance +4. Only then store the voucher + +**Request Body (without depositAuthorization):** +```json +{ + "paymentPayload": { + "x402Version": 1, + "network": "base-sepolia", + "scheme": "deferred", + "payload": { + "signature": "0x4b3f8e...", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "6000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673100, + "nonce": 3, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + } + } + }, + "paymentRequirements": { + "x402Version": 1, + "network": "base-sepolia", + "scheme": "deferred", + "recipient": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "amount": "1000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "extra": { + "type": "aggregation", + "signature": "0x3a2f7e3b...", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "5000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673000, + "nonce": 2, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + } + } + } +} +``` + +**Request Body (with depositAuthorization):** +```json +{ + "paymentPayload": { + "x402Version": 1, + "network": "base-sepolia", + "scheme": "deferred", + "payload": { + "signature": "0x4b3f8e...", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "1000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673000, + "nonce": 1, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + }, + "depositAuthorization": { + "permit": { + "owner": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "spender": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "value": "5000000", + "nonce": "0", + "deadline": 1740759400, + "domain": { + "name": "USD Coin", + "version": "2" + }, + "signature": "0x8f9e2a3b..." + }, + "depositAuthorization": { + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "amount": "5000000", + "nonce": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "expiry": 1740759400, + "signature": "0xbfdc3d0a..." + } + } + } + }, + "paymentRequirements": { + "x402Version": 1, + "network": "base-sepolia", + "scheme": "deferred", + "recipient": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "amount": "1000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "extra": { + "type": "new", + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27" + } + } + } +} +``` + +**Response (201 Created):** +```json +{ + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "6000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673100, + "nonce": 3, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + "signature": "0x4b3f8e..." +} +``` + +**Response (400 Bad Request):** +```json +{ + "isValid": false, + "invalidReason": "invalid_deferred_evm_payload_signature", + "payer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C" +} +``` + +### POST /vouchers/:id/:nonce/settle + +Initiates on-chain settlement of a voucher by calling the escrow contract's `collect` function. + +**Request Body (optional):** +```json +{ + "gasPrice": "20000000000", + "gasLimit": "150000" +} +``` + +**Response (200 OK):** +```json +{ + "success": true, + "transactionHash": "0xabc123...", + "collectedAmount": "6000000", + "network": "base-sepolia" +} +``` + +**Response (400 Bad Request):** +```json +{ + "success": false, + "error": "Voucher not found" +} +``` + +## Optional APIs + +These endpoints are not required for the x402 deferred handshake between a buyer and a seller but might come in handy for audit, visualization or observability purposes. + +### GET /vouchers/:id/:nonce + +Retrieves a specific voucher by ID and nonce. + +**Response (200 OK):** +```json +{ + "voucher": { /* voucher fields */ }, + "signature": "0x3a2f7e3b..." +} +``` + +### GET /vouchers/:id + +Retrieves all vouchers in a series, sorted by nonce (descending). + +**Query Parameters:** +- `limit` (optional): Maximum results (default: 100) +- `offset` (optional): Pagination offset (default: 0) + +**Response (200 OK):** +```json +{ + "vouchers": [ + { + "voucher": { /* voucher fields */ }, + "signature": "0x4b3f8e..." + } + ], + "pagination": { + "limit": 100, + "offset": 0, + "total": 5 + } +} +``` + +### GET /vouchers + +Queries vouchers with filtering. + +**Query Parameters:** +- `buyer` (optional): Filter by buyer address +- `seller` (optional): Filter by seller address +- `latest` (optional): If true, return only highest nonce per series +- `limit` (optional): Maximum results (default: 100) +- `offset` (optional): Pagination offset (default: 0) + +**Response (200 OK):** +```json +{ + "vouchers": [ + { + "voucher": { /* voucher fields */ }, + "signature": "0x3a2f7e3b..." + } + ], + "pagination": { + "limit": 100, + "offset": 0, + "total": 42 + } +} +``` + +### POST /vouchers/:id/:nonce/verify + +Verifies a voucher's validity without settling it. + +**Response (200 OK):** +```json +{ + "valid": true, + "escrowBalance": "10000000", + "collectableAmount": "6000000", + "alreadyCollected": "0" +} +``` + +### GET /vouchers/:id/:nonce/collections + +Retrieves settlement history for a voucher. + +**Query Parameters:** +- `limit` (optional): Maximum results (default: 100) +- `offset` (optional): Pagination offset (default: 0) + +**Response (200 OK):** +```json +{ + "collections": [ + { + "voucherId": "0x9f8d3e4a...", + "voucherNonce": 3, + "transactionHash": "0xabc123...", + "collectedAmount": "6000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "chainId": 84532, + "collectedAt": 1740673200 + } + ], + "pagination": { + "limit": 100, + "offset": 0, + "total": 1 + } +} +``` + + +### GET /vouchers/available/:buyer/:seller + +Returns the most suitable voucher for aggregation between a buyer-seller pair. + +**Response (200 OK):** +```json +{ + "voucher": { + "id": "0x9f8d3e4a2c7b9d04dcd11c9f4c2b22b0a6f87671e7b8c3a2ea95b5dbdf4040bc", + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "valueAggregate": "5000000", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "timestamp": 1740673000, + "nonce": 2, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 + }, + "signature": "0x3a2f7e3b..." +} +``` + +**Response (404 Not Found):** No vouchers exist for this pair + +### POST /buyers/:buyer/flush + +Flushes an escrow account using a signed flush authorization. This operation allows a buyer to authorize the facilitator to help them recover escrowed funds by: +1. Withdrawing any funds that have completed their thawing period +2. Initiating thawing for any remaining balance + +The flush authorization can be either: +- **Specific flush**: When `seller` and `asset` are provided, flushes only that specific account +- **Flush all**: When `seller` or `asset` are undefined, flushes all escrow accounts for the buyer + +**Request Body:** +```json +{ + "flushAuthorization": { + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "seller": "0xA1c7Bf3d421e8A54D39FbBE13f9f826E5B2C8e3D", + "asset": "0x081827b8c3aa05287b5aa2bc3051fbe638f33152", + "nonce": "0x0000000000000000000000000000000000000000000000000000000000000000", + "expiry": 1740759400, + "signature": "0xbfdc3d0a..." + }, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 +} +``` + +**Request Body (Flush All - seller/asset undefined):** +```json +{ + "flushAuthorization": { + "buyer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "nonce": "0x0000000000000000000000000000000000000000000000000000000000000000", + "expiry": 1740759400, + "signature": "0xbfdc3d0a..." + }, + "escrow": "0x7cB1A5A2a2C9e91B76914C0A7b7Fb3AefF3BCA27", + "chainId": 84532 +} +``` + +**Response (200 OK):** +```json +{ + "success": true, + "transaction": "0xabc123...", + "payer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C" +} +``` + +**Response (400 Bad Request):** +```json +{ + "success": false, + "errorReason": "invalid_deferred_evm_payload_flush_authorization_signature", + "transaction": "", + "payer": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C" +} +``` + diff --git a/specs/schemes/deferred/scheme_deferred_evm_voucher_store.md b/specs/schemes/deferred/scheme_deferred_evm_voucher_store.md new file mode 100644 index 0000000000..395e5ef335 --- /dev/null +++ b/specs/schemes/deferred/scheme_deferred_evm_voucher_store.md @@ -0,0 +1,167 @@ +# Voucher Store Specification + +## Summary + +The Voucher Store is a critical component of the x402 `deferred` payment scheme that manages the persistence and retrieval of signed payment vouchers and their settlement records. It serves as the data layer for sellers and facilitators to track off-chain payment obligations and their eventual on-chain settlements. + +This specification defines the interface and requirements for implementing a voucher store in the deferred EVM payment system, ensuring consistent behavior across different implementations (in-memory, database-backed, etc.). + +## Overview + +The voucher store manages three key concepts: + +1. **Vouchers**: EIP-712 signed payment commitments from buyers to sellers +2. **Voucher Series**: A sequence of vouchers sharing the same ID but with different nonces (representing aggregations) +3. **Collections**: Records of on-chain settlements for vouchers + +## Data Model + +### Voucher Structure + +A voucher contains the following fields: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | bytes32 | Unique identifier for the voucher series | +| `buyer` | address | Address of the payment initiator | +| `seller` | address | Address of the payment recipient | +| `valueAggregate` | uint256 | Total accumulated amount (monotonically increasing) | +| `asset` | address | ERC-20 token contract address | +| `timestamp` | uint64 | Last aggregation timestamp | +| `nonce` | uint256 | Incremented with each aggregation | +| `escrow` | address | Address of the escrow contract | +| `chainId` | uint256 | Network chain ID | +| `signature` | bytes | EIP-712 signature of the voucher | + +### Collection Structure + +A collection record contains: + +| Field | Type | Description | +|-------|------|-------------| +| `voucherId` | bytes32 | The voucher series ID | +| `voucherNonce` | uint256 | The specific voucher nonce | +| `transactionHash` | bytes32 | On-chain settlement transaction hash | +| `collectedAmount` | uint256 | Amount actually collected on-chain | +| `asset` | address | ERC-20 token contract address | +| `chainId` | uint256 | Network chain ID | +| `collectedAt` | uint64 | Collection timestamp | + +## Core Operations + +### 1. Voucher Storage + +**Operation**: `storeVoucher(voucher)` + +**Purpose**: Persist a new signed voucher received from a buyer. + +**Requirements**: +- MUST reject duplicate vouchers (same id + nonce combination) +- MUST validate all required fields are present +- SHOULD validate signature format (but not cryptographic validity) +- MUST return error if storage fails + +**Use Case**: When a seller receives a new payment voucher from a buyer, either for a new series or an aggregation of an existing series. + +### 2. Voucher Retrieval + +#### Single Voucher Lookup + +**Operation**: `getVoucher(id, nonce?)` + +**Purpose**: Retrieve a specific voucher or the latest in a series. + +**Behavior**: +- When `nonce` provided: Return exact voucher matching (id, nonce) +- When `nonce` omitted: Return voucher with highest nonce for the given id +- Return `null` if no matching voucher exists + +**Use Case**: Get the details of a voucher. + +#### Series Retrieval + +**Operation**: `getVoucherSeries(id, pagination)` + +**Purpose**: Retrieve all vouchers in a series for audit or history tracking. + +**Requirements**: +- MUST return vouchers sorted by nonce (descending - newest first) +- MUST support pagination with configurable limit and offset +- MUST return empty array for non-existent series + +**Pagination Options**: +- `limit`: The maximum number of vouchers to return +- `offset`: The offset of the first voucher to return + +**Use Case**: Display payment history, audit trail, or analyze aggregation patterns. + +#### Query-Based Retrieval + +**Operation**: `getVouchers(query, pagination)` + +**Purpose**: Find vouchers matching specific criteria. + +**Query Options**: +- `buyer`: Filter by buyer address +- `seller`: Filter by seller address +- `latest`: If true, return only highest nonce per series + +**Pagination Options**: +- `limit`: The maximum number of vouchers to return +- `offset`: The offset of the first voucher to return + +**Sorting**: +- Primary: By nonce (descending) +- Secondary: By timestamp (descending) + +**Use Case**: Dashboard views, account reconciliation, payment analytics. + +### 3. Available Voucher Discovery + +**Operation**: `getAvailableVoucher(buyer, seller)` + +**Purpose**: Find the most suitable voucher for aggregation in a new payment. + +**Selection Algorithm**: +1. Filter vouchers matching exact buyer and seller +2. For each series, select the voucher with highest nonce +3. Among selected vouchers, return the one with most recent timestamp +4. Return `null` if no vouchers match + +**Use Case**: When a seller needs to determine which existing voucher to use to aggregate new payments from a returning buyer. + +### 4. Settlement Recording + +**Operation**: `settleVoucher(voucher, txHash, amount)` + +**Purpose**: Record that a voucher has been collected on-chain. + +**Requirements**: +- MUST store the settlement record +- MUST associate with correct voucher (id, nonce) +- MUST record actual collected amount (may differ from voucher amount) +- SHOULD allow multiple collections for same voucher (partial settlements) + +**Use Case**: After successful on-chain collection, record the settlement for reconciliation and tracking. + +### 5. Collection History + +**Operation**: `getVoucherCollections(query, pagination)` + +**Purpose**: Retrieve settlement history for vouchers. + +**Query Options**: +- `id`: Filter by voucher series ID +- `nonce`: Filter by specific nonce (requires id) + +**Pagination Options**: +- `limit`: The maximum number of vouchers to return +- `offset`: The offset of the first voucher to return + +**Use Case**: Reconcile on-chain settlements with off-chain vouchers, audit payment flows. + +## Appendix + +### Reference Implementation + +The `InMemoryVoucherStore` class in the x402 TypeScript package provides a reference implementation suitable for development and testing. Production implementations should follow the same interface while adding appropriate persistence, scaling, and security features. \ No newline at end of file