From 04097ba7756da374ea79c554ce20d42be11a3eea Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 19 Mar 2026 00:31:01 -0700 Subject: [PATCH] Update protocol and contracts docs to match current state - Rewrite escrow scheme spec to align with x402 PR #1425 and issue #1011 (settlement methods, nonce derivation, verification/settlement logic, error codes, PaymentInfo struct, expiry ordering) - Update all contract addresses to unified CREATE3 (same on every chain) - Add missing contracts: ArbiterRegistry, RefundRequestEvidence, ReceiverRefundCollector, SignatureCondition, SignatureRefundRequest - Add all 11 supported chains to index page - Fix PaymentInfo struct in architecture docs (was missing fields) - Fix payment flow diagrams to route through operator (not escrow directly) - Update factories with CREATE3 addresses and additional factory list - Update roadmap with escrow scheme spec submission status - Fix comparison hybrid example with correct escrow extra fields Co-Authored-By: Claude Opus 4.6 (1M context) --- contracts/architecture.mdx | 21 +- contracts/examples.mdx | 2 +- contracts/factories.mdx | 21 +- contracts/fees.mdx | 2 +- contracts/gas-costs.mdx | 2 +- contracts/overview.mdx | 28 +- ...ore-contracts.mdx => payment-operator.mdx} | 375 ++--------------- contracts/periphery/auth-capture-escrow.mdx | 167 ++++++++ contracts/periphery/overview.mdx | 70 +++ .../periphery/receiver-refund-collector.mdx | 29 ++ .../periphery/refund-request-evidence.mdx | 24 ++ contracts/periphery/refund-request.mdx | 154 +++++++ docs.json | 12 +- index.mdx | 23 +- roadmap.mdx | 40 +- sdk/arbiter/decision-submission.mdx | 2 +- sdk/merchant/payment-operations.mdx | 2 +- sdk/merchant/quickstart.mdx | 2 +- sdk/merchant/refund-handling.mdx | 2 +- x402-integration/comparison.mdx | 10 +- x402-integration/escrow-scheme.mdx | 398 ++++++++++-------- x402-integration/overview.mdx | 13 +- 22 files changed, 812 insertions(+), 587 deletions(-) rename contracts/{core-contracts.mdx => payment-operator.mdx} (51%) create mode 100644 contracts/periphery/auth-capture-escrow.mdx create mode 100644 contracts/periphery/overview.mdx create mode 100644 contracts/periphery/receiver-refund-collector.mdx create mode 100644 contracts/periphery/refund-request-evidence.mdx create mode 100644 contracts/periphery/refund-request.mdx diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index 026c841..c5f5458 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -158,15 +158,18 @@ This example shows marketplace configuration. For subscriptions, you might use ` // PaymentInfo is from base commerce-payments (AuthCaptureEscrow) // Passed as calldata to operator methods - not stored in operator struct PaymentInfo { - address payer; - address receiver; address operator; // The PaymentOperator address - uint256 amount; - address token; - uint48 authorizationExpiry; // When authorization expires - uint16 minFeeBps; // Minimum fee in basis points - uint16 maxFeeBps; // Maximum fee in basis points + address payer; // Client wallet + address receiver; // Fund recipient + address token; // ERC-20 token address + uint120 maxAmount; // Maximum authorized amount + uint48 preApprovalExpiry; // ERC-3009 validBefore / pre-approval deadline + uint48 authorizationExpiry;// Capture deadline (authorize path only) + uint48 refundExpiry; // Refund request deadline + uint16 minFeeBps; // Minimum fee in basis points + uint16 maxFeeBps; // Maximum fee in basis points address feeReceiver; // Who receives fees (operator itself) + uint256 salt; // Client-provided entropy } ``` @@ -306,8 +309,8 @@ These events enable off-chain monitoring and indexing. ## Next Steps - - Learn about individual contract implementations. + + Learn about the core operator contract. Explore the condition system and combinators. diff --git a/contracts/examples.mdx b/contracts/examples.mdx index b322c77..136798a 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -851,7 +851,7 @@ Keep records of deployed configurations: ## Next Steps - + Review contract methods and security features. diff --git a/contracts/factories.mdx b/contracts/factories.mdx index eadbe35..3a0b77e 100644 --- a/contracts/factories.mdx +++ b/contracts/factories.mdx @@ -46,9 +46,9 @@ Deploys PaymentOperator instances with deterministic addresses. ### Contract Address -**Base Sepolia:** `0xe01CEd771A30A23a7A4C9c1db604C74D4Dc4ebe8` +All factories use unified CREATE3 addresses (same on every chain). -**Base Mainnet:** `0xA50F51254E8B08899EdB76Bd24b4DC6A61ba7dE7` +**PaymentOperatorFactory:** `0x4D9BC2Ba2D0d9AFb6B63E3afBbfC95143E6E8Da9` ### Configuration Structure @@ -208,9 +208,7 @@ Deploys `EscrowPeriod` contracts - combined recorder and condition for time-base ### Contract Address -**Base Sepolia:** `0x206D4DbB6E7b876e4B5EFAAD2a04e7d7813FB6ba` - -**Base Mainnet:** `0x2714EA3e839Ac50F52B2e2a5788F614cACeE5316` +**EscrowPeriodFactory:** `0x15DB06aADEB3a39D47756Bf864a173cc48bafe24` ### Deployment Method @@ -299,8 +297,7 @@ Deploys `Freeze` condition contracts that block release when a payment is frozen ### Contract Address -**Base Sepolia:** `0x199fed16577773Bb6b2D76CC3cD1c76c22D28835` -**Base Mainnet:** `0xCAEd9474c06bf9139AC36C874dED838e1Bcb9310` +**FreezeFactory:** `0xdf129EFFE040c3403aca597c0F0bb704859a78Fd` ### Deployment Method @@ -355,11 +352,11 @@ const config = { Use these pre-deployed condition contracts: -| Condition | Address (Base Sepolia) | Address (Base Mainnet) | Description | -|-----------|------------------------|------------------------|-------------| -| PayerCondition | `0xBAF68176FF94CAdD403EF7FbB776bbca548AC09D` | `0xb33D6502EdBbC47201cd1E53C49d703EC0a660b8` | Only payer can call | -| ReceiverCondition | `0x12EDefd4549c53497689067f165c0f101796Eb6D` | `0xed02d3E5167BCc9582D851885A89b050AB816a56` | Only receiver can call | -| AlwaysTrueCondition | `0x785cC83DEa3d46D5509f3bf7496EAb26D42EE610` | `0xc9BbA6A2CF9838e7Dd8c19BC8B3BAC620B9D8178` | Anyone can call | +| Condition | Address (all chains) | Description | +|-----------|---------------------|-------------| +| PayerCondition | `0x33F5F1154A02d0839266EFd23Fd3b85a3505bB4B` | Only payer can call | +| ReceiverCondition | `0xF41974A853940Ff4c18d46B6565f973c1180E171` | Only receiver can call | +| AlwaysTrueCondition | `0xb295df7E7f786fd84D614AB26b1f2e86026C3483` | Anyone can call | ### Example Deployments diff --git a/contracts/fees.mdx b/contracts/fees.mdx index 949acbf..5e08280 100644 --- a/contracts/fees.mdx +++ b/contracts/fees.mdx @@ -234,7 +234,7 @@ The operator's `FEE_RECIPIENT` varies by use case: ## Next Steps - + See how fees integrate with PaymentOperator. diff --git a/contracts/gas-costs.mdx b/contracts/gas-costs.mdx index 57fbe7d..486f1e1 100644 --- a/contracts/gas-costs.mdx +++ b/contracts/gas-costs.mdx @@ -6,7 +6,7 @@ icon: "gas-pump" ## Overview -x402r adds escrow, refund windows, and dispute resolution on top of [Base Commerce Payments](https://github.com/base/commerce-payments). Here you'll find the **measured gas cost** of every on-chain operation so you can evaluate the overhead. +x402r adds escrow, refund windows, and dispute resolution on top of the [Commerce Payments Protocol](https://github.com/base/commerce-payments). Here you'll find the **measured gas cost** of every on-chain operation so you can evaluate the overhead. All numbers are from Foundry simulations (`forge test`) with optimizer enabled (200 runs, via IR). The benchmark test is at [`test/gas/GasBenchmark.t.sol`](https://github.com/BackTrackCo/x402r-contracts/blob/main/test/gas/GasBenchmark.t.sol). diff --git a/contracts/overview.mdx b/contracts/overview.mdx index 95937bc..2d773c4 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -106,16 +106,25 @@ x402r extends commerce-payments with flexible payment capabilities: - MEV protection via private mempool support - Composable via `AndCondition([escrowPeriod, freeze])` -### 4. Factory Pattern +### 4. Arbiter & Evidence System -**PaymentOperatorFactory** - Deploys operators with deterministic CREATE2 addresses +**RefundRequestEvidence** - On-chain evidence submission with IPFS CIDs and arbiter EIP-712 signature approval + +### 5. Factory Pattern + +**PaymentOperatorFactory** - Deploys operators with deterministic CREATE3 addresses **EscrowPeriodFactory** - Deploys EscrowPeriod contracts **FreezeFactory** - Deploys Freeze condition contracts +Plus factories for: StaticFeeCalculator, StaticAddressCondition, AndCondition, OrCondition, NotCondition, RecorderCombinator. + +All factories use **unified CREATE3 addresses** — same address on every supported chain. + **Benefits:** - Predictable addresses for off-chain address generation +- Cross-chain address consistency (same config = same address on every chain) - Shared configuration reduces deployment costs - Idempotent deployments (same config = same address) - Owner controls all deployed instances via factory @@ -126,11 +135,12 @@ x402r extends commerce-payments with flexible payment capabilities: |---------|------------------|-------| | **Refunds** | Manual void/reclaim | Structured refund requests with configurable approval | | **Escrow Period** | Not enforced | Configurable time-lock before release | -| **Dispute Resolution** | Not built-in | Optional arbiter workflow via conditions | -| **Authorization** | Operator-based only | Pluggable conditions (access, time, combinators) | +| **Dispute Resolution** | Not built-in | Arbiter workflow via conditions, signatures, and evidence | +| **Authorization** | Operator-based only | Pluggable conditions (access, time, signature, combinators) | | **Freeze Mechanism** | Not available | Configurable freeze during escrow period | -| **Deployment** | Direct deployment | Factory pattern with CREATE2 | -| **Fees** | Not enforced | Configurable protocol and operator fees | +| **Deployment** | Direct deployment | Factory pattern with unified CREATE3 (same address every chain) | +| **Fees** | Not enforced | Additive protocol + operator fees with 7-day timelock | +| **Multi-chain** | Per-chain deployment | Unified CREATE3 addresses across 11 chains | ## Use Cases @@ -184,7 +194,7 @@ Protocol fee configuration is mutable via `ProtocolFeeConfig` (with 7-day timelo For a detailed view of how all contracts interact, see [Architecture](/contracts/architecture). -To understand individual contracts, see [Core Contracts](/contracts/core-contracts). +To understand the core operator, see [PaymentOperator](/contracts/payment-operator). For supporting contracts, see [Periphery](/contracts/periphery/overview). For factory deployment patterns, see [Factories](/contracts/factories). @@ -194,8 +204,8 @@ For factory deployment patterns, see [Factories](/contracts/factories). View system architecture diagrams and payment flows. - - Learn about PaymentOperator, RefundRequest, and other core contracts. + + Learn about the core operator contract. Deploy a PaymentOperator using the SDK. diff --git a/contracts/core-contracts.mdx b/contracts/payment-operator.mdx similarity index 51% rename from contracts/core-contracts.mdx rename to contracts/payment-operator.mdx index a968343..8e4a827 100644 --- a/contracts/core-contracts.mdx +++ b/contracts/payment-operator.mdx @@ -1,22 +1,20 @@ --- -title: "Core Contracts" -description: "Detailed explanation of PaymentOperator, RefundRequest, and AuthCaptureEscrow" +title: "PaymentOperator" +description: "The core payment operator contract with pluggable conditions and fee management" icon: "file-contract" --- -## PaymentOperator +## Overview The main payment operator contract with pluggable conditions for flexible authorization logic. -### Overview - - **Type:** Operator instance (one per fee recipient + configuration) - **Deployment:** Via PaymentOperatorFactory - **Immutability:** Cannot be paused or upgraded - **Configuration:** 10 slots for conditions and recorders - **Use Cases:** Marketplace, subscriptions, streaming, grants, custom flows -### Immutable Fields +## Immutable Fields ```solidity address public immutable ESCROW; // AuthCaptureEscrow address @@ -25,7 +23,7 @@ ProtocolFeeConfig public immutable PROTOCOL_FEE_CONFIG; // Shared protocol fee c IFeeCalculator public immutable FEE_CALCULATOR; // Operator fee calculator ``` -### State +## State ```solidity // Fee tracking for accurate distribution @@ -35,7 +33,7 @@ mapping(address token => uint256) public accumulatedProtocolFees; mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; ``` -### 10-Slot Configuration +## 10-Slot Configuration 1. **AUTHORIZE_CONDITION** - Who can authorize payments @@ -57,9 +55,9 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; **Default:** `address(0)` = no recording (no-op) -### Key Methods +## Key Methods -#### authorize() +### authorize() Authorizes a payment and locks funds in escrow. @@ -92,7 +90,7 @@ function authorize( **Authorization Expiry:** The `PaymentInfo` struct includes an `authorizationExpiry` field (from base commerce-payments). Set this to `type(uint48).max` for no expiry, or specify a timestamp to allow the payer to reclaim funds after expiry. This is useful for subscription-based payments where you want to limit the authorization window. -#### charge() +### charge() Direct charge - collects payment and immediately transfers to receiver (no escrow hold). @@ -125,7 +123,7 @@ function charge( Unlike `authorize()`, funds go directly to receiver without escrow hold. Refunds are only possible via `refundPostEscrow()`. -#### release() +### release() Releases funds from escrow to receiver (capture). @@ -156,7 +154,7 @@ function release( **DAO example:** StaticAddressCondition(daoMultisig) -#### refundInEscrow() +### refundInEscrow() Refunds payment while still in escrow (partial void). @@ -186,7 +184,7 @@ function refundInEscrow( **Subscription example:** address(0) - no refunds -#### refundPostEscrow() +### refundPostEscrow() Refunds payment after it has been released (captured). @@ -219,11 +217,11 @@ function refundPostEscrow( **Most configurations:** address(0) - no post-escrow refunds -### Fee System (Modular, Additive) +## Fee System (Modular, Additive) Fees are additive and modular: `totalFee = protocolFee + operatorFee` -#### Fee Architecture +### Fee Architecture ```solidity // Shared protocol fee config (timelocked, swappable calculator) @@ -243,8 +241,8 @@ mapping(address token => uint256) public accumulatedProtocolFees; **Example Fee Calculation (Additive):** For a 1000 USDC payment: -- **Protocol Fee:** 3 bps (0.03%) = 0.30 USDC → goes to `protocolFeeRecipient` -- **Operator Fee:** 2 bps (0.02%) = 0.20 USDC → goes to `FEE_RECIPIENT` +- **Protocol Fee:** 3 bps (0.03%) = 0.30 USDC -> goes to `protocolFeeRecipient` +- **Operator Fee:** 2 bps (0.02%) = 0.20 USDC -> goes to `FEE_RECIPIENT` - **Total Fee:** 5 bps (0.05%) = 0.50 USDC - **Receiver Gets:** 999.50 USDC @@ -265,18 +263,18 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; - Platform Treasury (platform-controlled) - DAO Multisig (governance-controlled) -#### Fee Distribution +### Fee Distribution Fees accumulate in the operator contract and are distributed via `distributeFees(token)`: ```solidity // Anyone can call to distribute fees for a token operator.distributeFees(usdcAddress); -// Protocol share → protocolFeeRecipient -// Operator share → FEE_RECIPIENT +// Protocol share -> protocolFeeRecipient +// Operator share -> FEE_RECIPIENT ``` -#### Protocol Fee Changes (7-day Timelock) +### Protocol Fee Changes (7-day Timelock) Protocol fee calculator changes require a 7-day timelock on `ProtocolFeeConfig`: @@ -294,347 +292,24 @@ protocolFeeConfig.executeCalculator(); Protocol fee changes require 7-day timelock. Operator fees are immutable (set at deploy time). -### Security Features +## Security Features - **ReentrancyGuardTransient** - EIP-1153 transient storage for gas-efficient reentrancy protection - **Ownership** - Solady's Ownable with 2-step transfer - **Timelock** - 7-day delay on protocol fee changes (operator fees are immutable) - **Immutable Core** - Escrow, conditions, and fee configuration cannot be changed ---- - -## RefundRequest - -Manages refund requests independent of operator implementation. - -### Overview - -- **Type:** Singleton (one per network) -- **Deployment:** Direct deployment (no factory) -- **Purpose:** Track refund request lifecycle - -### Request Types - - - - **Who can request:** Payer, Receiver, OR Arbiter - - **Typical flow:** - 1. Payer suspects fraud, requests refund - 2. Arbiter investigates - 3. Arbiter approves or denies request - 4. If approved, arbiter calls `operator.refundInEscrow()` - - **Use cases:** - - Buyer remorse - - Seller fraud - - Payment error - - - - **Who can request:** Receiver only - - **Typical flow:** - 1. Receiver realizes product defect after release - 2. Receiver requests refund - 3. Arbiter investigates - 4. If approved, arbiter calls `operator.refundPostEscrow()` - - **Use cases:** - - Product defects discovered later - - Service not as described - - Voluntary refund by merchant - - - -### Request Status States - -```mermaid -stateDiagram-v2 - [*] --> Pending - Pending --> Approved: Arbiter approves - Pending --> Denied: Arbiter denies - Pending --> Cancelled: Requester cancels - Approved --> [*]: Arbiter executes refund via operator - - note right of Approved - Arbiter must call - operator.refundInEscrow() - or operator.refundPostEscrow() - end note -``` - -### Key Methods - -#### requestRefund() - -Creates a new refund request. - -```solidity -function requestRefund( - AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint120 amount, - uint256 nonce -) external -``` - -**Parameters:** -- `paymentInfo` - Payment info struct -- `amount` - Amount being requested for refund -- `nonce` - Record index (from PaymentIndexRecorder) identifying which charge/action - -**Access Control:** Only the payer who made the authorization can request - - -Each refund request is keyed by `(paymentInfoHash, nonce)` where nonce is the record index. This allows multiple refund requests per payment (one per charge/action). - - -#### updateStatus() - -Approve or deny a refund request. - -```solidity -function updateStatus( - AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint256 nonce, - RequestStatus newStatus -) external -``` - -**Parameters:** -- `paymentInfo` - Payment info struct -- `nonce` - Record index identifying which refund request -- `newStatus` - The new status (`Approved` or `Denied`) - -**Access:** Receiver can always approve/deny. While in escrow, anyone passing the operator's `REFUND_IN_ESCROW_CONDITION` can also approve/deny. - -**Valid transitions:** -- `Pending` → `Approved` -- `Pending` → `Denied` - -#### cancelRefundRequest() - -Payer cancels their own request. - -```solidity -function cancelRefundRequest( - AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint256 nonce -) external -``` - -**Parameters:** -- `paymentInfo` - Payment info struct -- `nonce` - Record index identifying which refund request - -**Access:** Only the payer who created the request - -**Valid transition:** -- `Pending` → `Cancelled` - -### Usage Example - -```typescript -// 1. Payer requests refund (nonce 0 = first action on this payment) -await refundRequest.requestRefund(paymentInfo, requestedAmount, 0); -// Status: Pending - -// 2. Receiver (or arbiter) reviews and approves -await refundRequest.updateStatus( - paymentInfo, - 0, // nonce - RequestStatus.Approved -); -// Status: Approved - -// 3. Execute refund via operator (separate transaction) -await operator.refundInEscrow(paymentInfo, refundAmount); -// Funds returned to payer -``` - - -RefundRequest is **advisory only**. Approval does not automatically execute refunds - the authorized party must call the operator's refund function. - - ---- - -## AuthCaptureEscrow - -Base escrow contract from commerce-payments library. - -### Overview - -- **Type:** Singleton (one per network) -- **Source:** [commerce-payments](https://github.com/BackTrackCo/commerce-payments) (x402r fork) -- **Purpose:** Hold ERC-20 tokens during payment lifecycle -- **Access:** Operator-based (only registered operators can manage payments) - - -x402r uses a forked version of commerce-payments with added **partial void** support for handling partially completed orders. - - -### Payment State Machine - -```mermaid -stateDiagram-v2 - [*] --> NonExistent - NonExistent --> InEscrow: authorize() - InEscrow --> Released: release() - InEscrow --> Settled: void() / refundInEscrow() - Released --> Settled: reclaim() / refundPostEscrow() - Settled --> [*] - - note right of InEscrow - Funds locked in escrow - Payer can reclaim after expiry - end note - - note right of Released - Funds transferred to receiver - Can still refund post-escrow - end note - - note right of Settled - Terminal state - No further actions possible - end note -``` - -### Key Methods - -#### authorize() - -Locks tokens in escrow. Called by operator. - -```solidity -function authorize( - bytes32 paymentId, - address payer, - address receiver, - uint256 amount, - address token, - address operator -) external onlyOperator -``` - -**Requires:** Payer has approved escrow contract for `amount` of `token` - - -The base escrow contract uses individual parameters (paymentId, payer, receiver, etc.) while the PaymentOperator wraps them in a `PaymentInfo` struct. The operator translates between the two formats internally. - - -#### release() - -Releases tokens to receiver. Called by operator. - -```solidity -function release( - bytes32 paymentId -) external onlyOperator returns (uint256 amount) -``` - -**State change:** `InEscrow` → `Released` - -#### void() - -Returns tokens to payer (full refund). Called by operator. - -```solidity -function void( - bytes32 paymentId -) external onlyOperator -``` - -**State change:** `InEscrow` → `Settled` - -**Use case:** Refund during escrow period - -#### reclaim() - -Takes tokens back from receiver to give to payer. Called by operator. - -```solidity -function reclaim( - bytes32 paymentId, - address from, - uint256 amount -) external onlyOperator -``` - -**State change:** `Released` → `Settled` - -**Use case:** Refund after release - -**Requires:** Receiver has approved escrow for `amount` - -#### partialVoid() - -Returns partial amount to payer. - -```solidity -function partialVoid( - bytes32 paymentId, - uint256 amount -) external onlyOperator -``` - -**Use case:** Partial refunds for partially fulfilled orders - -### Security Features - -- **Operator whitelist** - Only registered operators can manage payments -- **Reentrancy protection** - All state changes protected -- **Event logging** - Complete audit trail - ---- - -## Supporting Contracts - -### ERC3009PaymentCollector - -Payment collection with gasless meta-transactions. - -**Features:** -- ERC-3009 `transferWithAuthorization()` support -- Nonce-based replay protection -- Deadline-based expiry -- Multicall3 integration - -**Use case:** Allow payers to authorize payments via signed messages instead of direct transactions (gasless UX). - -### TokenStore - -Safe token transfer utilities from commerce-payments. - -**Features:** -- Reentrancy-safe transfers -- SafeERC20 integration -- Balance tracking - ---- - -## Contract Addresses - -### Base Sepolia Testnet - -| Contract | Address | -|----------|---------| -| AuthCaptureEscrow | [`0xb9488351E48b23D798f24e8174514F28B741Eb4f`](https://sepolia.basescan.org/address/0xb9488351E48b23D798f24e8174514F28B741Eb4f) | -| ERC3009PaymentCollector | [`0xed02d3E5167BCc9582D851885A89b050AB816a56`](https://sepolia.basescan.org/address/0xed02d3E5167BCc9582D851885A89b050AB816a56) | -| RefundRequest | [`0x6926c05193c714ED4bA3867Ee93d6816Fdc14128`](https://sepolia.basescan.org/address/0x6926c05193c714ED4bA3867Ee93d6816Fdc14128) | -| PaymentOperatorFactory | [`0xFa8C4Cb156053b867Ae7489220A29b5939E3Df70`](https://sepolia.basescan.org/address/0xFa8C4Cb156053b867Ae7489220A29b5939E3Df70) | - ## Next Steps - - Learn about factory deployment patterns. + + AuthCaptureEscrow, RefundRequest, and other supporting contracts. Explore the pluggable condition system. - - Deploy operators using the SDK's deployment utilities. + + Deploy operators with factory patterns. See real-world configuration examples. diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx new file mode 100644 index 0000000..aaf32bc --- /dev/null +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -0,0 +1,167 @@ +--- +title: "Commerce Payments" +description: "AuthCaptureEscrow and ERC3009PaymentCollector — the base layer from commerce-payments" +icon: "vault" +--- + +x402r builds on the [Commerce Payments Protocol](https://github.com/base/commerce-payments). Two contracts from this stack form the base layer: **AuthCaptureEscrow** (holds funds) and **ERC3009PaymentCollector** (collects funds via signed authorizations). + + +x402r uses a [fork of commerce-payments](https://github.com/BackTrackCo/commerce-payments) that adds **partial void** support for handling partially completed orders and partial refunds. + + +## AuthCaptureEscrow + +Core escrow contract for holding ERC-20 tokens during the payment lifecycle. + +- **Type:** Singleton (one per network) +- **Access:** Operator-based (only registered operators can manage payments) +- **Address:** `0xe050bB89eD43BB02d71343063824614A7fb80B77` (all chains) + +### Payment State Machine + +```mermaid +stateDiagram-v2 + [*] --> NonExistent + NonExistent --> InEscrow: authorize() + InEscrow --> Released: release() + InEscrow --> Settled: void() / refundInEscrow() + Released --> Settled: reclaim() / refundPostEscrow() + Settled --> [*] + + note right of InEscrow + Funds locked in escrow + Payer can reclaim after expiry + end note + + note right of Released + Funds transferred to receiver + Can still refund post-escrow + end note + + note right of Settled + Terminal state + No further actions possible + end note +``` + +### Key Methods + +#### authorize() + +Locks tokens in escrow. Called by operator. + +```solidity +function authorize( + bytes32 paymentId, + address payer, + address receiver, + uint256 amount, + address token, + address operator +) external onlyOperator +``` + +**Requires:** Payer has approved escrow contract for `amount` of `token` + + +The base escrow contract uses individual parameters (paymentId, payer, receiver, etc.) while the PaymentOperator wraps them in a `PaymentInfo` struct. The operator translates between the two formats internally. + + +#### release() + +Releases tokens to receiver. Called by operator. + +```solidity +function release( + bytes32 paymentId +) external onlyOperator returns (uint256 amount) +``` + +**State change:** `InEscrow` -> `Released` + +#### void() + +Returns tokens to payer (full refund). Called by operator. + +```solidity +function void( + bytes32 paymentId +) external onlyOperator +``` + +**State change:** `InEscrow` -> `Settled` + +#### reclaim() + +Takes tokens back from receiver to give to payer. Called by operator. + +```solidity +function reclaim( + bytes32 paymentId, + address from, + uint256 amount +) external onlyOperator +``` + +**State change:** `Released` -> `Settled` + +**Requires:** Receiver has approved escrow for `amount` + +#### partialVoid() + +Returns partial amount to payer (x402r addition). + +```solidity +function partialVoid( + bytes32 paymentId, + uint256 amount +) external onlyOperator +``` + +**Use case:** Partial refunds for partially fulfilled orders + +### Security Features + +- **Operator whitelist** - Only registered operators can manage payments +- **Reentrancy protection** - All state changes protected +- **Event logging** - Complete audit trail + +--- + +## ERC3009PaymentCollector + +Collects ERC-20 tokens into escrow using the client's off-chain ERC-3009 signature. The payer never submits a transaction. + +- **Type:** Singleton (one per network) +- **Address:** `0xcE66Ab399EDA513BD12760b6427C87D6602344a7` (all chains) + +### How It Works + +The operator calls the token collector during `authorize()` or `charge()`, passing the client's signature as `collectorData`. The collector executes `receiveWithAuthorization` (ERC-3009) to pull tokens from the payer into escrow. + +### Features + +- **ERC-3009 `receiveWithAuthorization()`** - Gasless token transfers via signed messages +- **EIP-6492 support** - Handles smart wallet clients with deployment bytecode in signatures +- **Nonce-based replay protection** - Each authorization can only be used once +- **Deadline-based expiry** - `validBefore` timestamp prevents stale authorizations + +### ERC-3009 Signature + +The client signs an EIP-712 typed data message with primary type `ReceiveWithAuthorization`: + +```typescript +const authorization = { + from: payerAddress, // Who is paying + to: tokenCollectorAddress, // ERC3009PaymentCollector + value: amount, // Amount in token decimals + validAfter: 0, // Earliest valid time (0 = immediately) + validBefore: deadline, // Latest valid time + nonce: derivedNonce // Deterministic nonce from payment params +}; +``` + + +The escrow scheme uses `receiveWithAuthorization` (not `transferWithAuthorization`). The token collector is the `to` address, which then routes tokens to the escrow contract. + diff --git a/contracts/periphery/overview.mdx b/contracts/periphery/overview.mdx new file mode 100644 index 0000000..ee95400 --- /dev/null +++ b/contracts/periphery/overview.mdx @@ -0,0 +1,70 @@ +--- +title: "Periphery Overview" +description: "Supporting contracts that extend the PaymentOperator: escrow, refund requests, token collectors, and more" +icon: "puzzle-piece" +--- + +## What Are Periphery Contracts? + +Periphery contracts support the [PaymentOperator](/contracts/payment-operator) but are not the operator itself. They handle escrow storage, token collection, refund workflows, and evidence submission. + +## Contract Map + +| Contract | Role | Type | +|----------|------|------| +| [Commerce Payments](/contracts/periphery/auth-capture-escrow) | AuthCaptureEscrow + ERC3009PaymentCollector (base layer) | Singleton | +| [RefundRequest](/contracts/periphery/refund-request) | Tracks refund request lifecycle and approvals | Singleton | +| [RefundRequestEvidence](/contracts/periphery/refund-request-evidence) | On-chain evidence submission for disputes | Singleton | +| [ReceiverRefundCollector](/contracts/periphery/receiver-refund-collector) | Pulls funds from receiver for post-escrow refunds | Singleton | + +## Contract Addresses + +All periphery contracts use **unified CREATE3 addresses** — the same address on every supported chain. + +| Contract | Address | +|----------|---------| +| AuthCaptureEscrow | `0xe050bB89eD43BB02d71343063824614A7fb80B77` | +| ERC3009PaymentCollector | `0xcE66Ab399EDA513BD12760b6427C87D6602344a7` | +| ProtocolFeeConfig | `0x7e868A42a458fa2443b6259419aA6A8a161E08c8` | +| ReceiverRefundCollector | `0xE5500a38BE45a6C598420fbd7867ac85EC451A07` | +| RefundRequestEvidence | `0xF97aAB816b7cbe53025454ad05b03cf5C361F1BA` | + +### Factories + +| Factory | Address | +|---------|---------| +| PaymentOperatorFactory | `0x4D9BC2Ba2D0d9AFb6B63E3afBbfC95143E6E8Da9` | +| EscrowPeriodFactory | `0x15DB06aADEB3a39D47756Bf864a173cc48bafe24` | +| FreezeFactory | `0xdf129EFFE040c3403aca597c0F0bb704859a78Fd` | +| StaticFeeCalculatorFactory | `0x6CDdBdB46e2d7Caae31A6b213B59a1412d7f16Ac` | +| StaticAddressConditionFactory | `0xfB09350b200fda7dDd06565F5296A0CA625311d5` | +| AndConditionFactory | `0x5a1F3b6d030D25a2B86aAE469Ae1216ef3be308D` | +| OrConditionFactory | `0x101B2fac8cdC6348E541A0ef087275dA62AA13A0` | +| NotConditionFactory | `0x1D58f97843579356863d3393ebe24feEd76ceefF` | +| RecorderCombinatorFactory | `0xACf2b5e21CFc14135C9cD43ebE96a481F184C1A1` | + +### Condition Singletons + +| Condition | Address | +|-----------|---------| +| PayerCondition | `0x33F5F1154A02d0839266EFd23Fd3b85a3505bB4B` | +| ReceiverCondition | `0xF41974A853940Ff4c18d46B6565f973c1180E171` | +| AlwaysTrueCondition | `0xb295df7E7f786fd84D614AB26b1f2e86026C3483` | + + +All addresses are available programmatically via `@x402r/core`'s `getChainConfig(chainId)`. See [SDK Installation](/sdk/installation) for details. + + +## Next Steps + + + + AuthCaptureEscrow and ERC3009PaymentCollector. + + + Refund request lifecycle and approvals. + + + The core operator contract. + + diff --git a/contracts/periphery/receiver-refund-collector.mdx b/contracts/periphery/receiver-refund-collector.mdx new file mode 100644 index 0000000..66640fa --- /dev/null +++ b/contracts/periphery/receiver-refund-collector.mdx @@ -0,0 +1,29 @@ +--- +title: "ReceiverRefundCollector" +description: "Pulls funds from receiver for post-escrow refunds" +icon: "arrow-rotate-left" +--- + +## Overview + +- **Type:** Singleton (one per network) +- **Purpose:** Collect tokens from the receiver to refund the payer after escrow release +- **Address:** `0xE5500a38BE45a6C598420fbd7867ac85EC451A07` (all chains) + +## Features + +- **Post-escrow refunds** - Pulls funds from the receiver's wallet after funds have already been released +- **Receiver approval required** - The receiver must have approved the collector contract or provided a signature +- **Operator integration** - Called by `operator.refundPostEscrow()` via the token collector interface + +## How It Works + +After funds have been released to the receiver (state: `Released`), refunds require pulling tokens back from the receiver's wallet. The `ReceiverRefundCollector` handles this by: + +1. Operator calls `refundPostEscrow(paymentInfo, amount, receiverRefundCollector, collectorData)` +2. The collector transfers tokens from the receiver to the escrow contract +3. The escrow contract returns tokens to the payer + + +The receiver must have pre-approved the `ReceiverRefundCollector` for token transfers, or the `collectorData` must contain a valid receiver signature authorizing the refund. + diff --git a/contracts/periphery/refund-request-evidence.mdx b/contracts/periphery/refund-request-evidence.mdx new file mode 100644 index 0000000..b799de7 --- /dev/null +++ b/contracts/periphery/refund-request-evidence.mdx @@ -0,0 +1,24 @@ +--- +title: "RefundRequestEvidence" +description: "On-chain evidence submission for refund disputes" +icon: "file-lines" +--- + +## Overview + +- **Type:** Singleton (one per network) +- **Purpose:** Store evidence for refund disputes on-chain +- **Address:** `0xF97aAB816b7cbe53025454ad05b03cf5C361F1BA` (all chains) + +## Features + +- **IPFS CID storage** - Stores content hashes on-chain for evidence trails +- **EIP-712 signature approval** - Arbiter can approve refunds via off-chain signatures (gas-free for arbiters) +- **Evidence indexing** - Evidence is indexed by payment and submitting party +- **Multi-party submission** - Both payer and receiver can submit evidence + +## How It Works + +When a refund is disputed, parties submit evidence (documents, screenshots, logs) to IPFS and record the CID on-chain. The arbiter reviews evidence off-chain and submits an EIP-712 approval signature that anyone can relay. + +This keeps dispute resolution costs low — the arbiter never needs to submit an on-chain transaction. diff --git a/contracts/periphery/refund-request.mdx b/contracts/periphery/refund-request.mdx new file mode 100644 index 0000000..0b6dc7c --- /dev/null +++ b/contracts/periphery/refund-request.mdx @@ -0,0 +1,154 @@ +--- +title: "RefundRequest" +description: "Manages refund request lifecycle and approvals independent of operator implementation" +icon: "rotate-left" +--- + +## Overview + +- **Type:** Singleton (one per network) +- **Deployment:** Direct deployment (no factory) +- **Purpose:** Track refund request lifecycle + +## Request Types + + + + **Who can request:** Payer, Receiver, OR Arbiter + + **Typical flow:** + 1. Payer suspects fraud, requests refund + 2. Arbiter investigates + 3. Arbiter approves or denies request + 4. If approved, arbiter calls `operator.refundInEscrow()` + + **Use cases:** + - Buyer remorse + - Seller fraud + - Payment error + + + + **Who can request:** Receiver only + + **Typical flow:** + 1. Receiver realizes product defect after release + 2. Receiver requests refund + 3. Arbiter investigates + 4. If approved, arbiter calls `operator.refundPostEscrow()` + + **Use cases:** + - Product defects discovered later + - Service not as described + - Voluntary refund by merchant + + + +## Request Status States + +```mermaid +stateDiagram-v2 + [*] --> Pending + Pending --> Approved: Arbiter approves + Pending --> Denied: Arbiter denies + Pending --> Cancelled: Requester cancels + Approved --> [*]: Arbiter executes refund via operator + + note right of Approved + Arbiter must call + operator.refundInEscrow() + or operator.refundPostEscrow() + end note +``` + +## Key Methods + +### requestRefund() + +Creates a new refund request. + +```solidity +function requestRefund( + AuthCaptureEscrow.PaymentInfo calldata paymentInfo, + uint120 amount, + uint256 nonce +) external +``` + +**Parameters:** +- `paymentInfo` - Payment info struct +- `amount` - Amount being requested for refund +- `nonce` - Record index (from PaymentIndexRecorder) identifying which charge/action + +**Access Control:** Only the payer who made the authorization can request + + +Each refund request is keyed by `(paymentInfoHash, nonce)` where nonce is the record index. This allows multiple refund requests per payment (one per charge/action). + + +### updateStatus() + +Approve or deny a refund request. + +```solidity +function updateStatus( + AuthCaptureEscrow.PaymentInfo calldata paymentInfo, + uint256 nonce, + RequestStatus newStatus +) external +``` + +**Parameters:** +- `paymentInfo` - Payment info struct +- `nonce` - Record index identifying which refund request +- `newStatus` - The new status (`Approved` or `Denied`) + +**Access:** Receiver can always approve/deny. While in escrow, anyone passing the operator's `REFUND_IN_ESCROW_CONDITION` can also approve/deny. + +**Valid transitions:** +- `Pending` -> `Approved` +- `Pending` -> `Denied` + +### cancelRefundRequest() + +Payer cancels their own request. + +```solidity +function cancelRefundRequest( + AuthCaptureEscrow.PaymentInfo calldata paymentInfo, + uint256 nonce +) external +``` + +**Parameters:** +- `paymentInfo` - Payment info struct +- `nonce` - Record index identifying which refund request + +**Access:** Only the payer who created the request + +**Valid transition:** +- `Pending` -> `Cancelled` + +## Usage Example + +```typescript +// 1. Payer requests refund (nonce 0 = first action on this payment) +await refundRequest.requestRefund(paymentInfo, requestedAmount, 0); +// Status: Pending + +// 2. Receiver (or arbiter) reviews and approves +await refundRequest.updateStatus( + paymentInfo, + 0, // nonce + RequestStatus.Approved +); +// Status: Approved + +// 3. Execute refund via operator (separate transaction) +await operator.refundInEscrow(paymentInfo, refundAmount); +// Funds returned to payer +``` + + +RefundRequest is **advisory only**. Approval does not automatically execute refunds - the authorized party must call the operator's refund function. + diff --git a/docs.json b/docs.json index e10378b..d9989ee 100644 --- a/docs.json +++ b/docs.json @@ -38,7 +38,8 @@ "pages": [ "contracts/overview", "contracts/architecture", - "contracts/core-contracts", + "contracts/periphery/auth-capture-escrow", + "contracts/payment-operator", "contracts/factories", "contracts/fees", "contracts/gas-costs", @@ -47,6 +48,15 @@ "contracts/license" ] }, + { + "group": "Periphery", + "pages": [ + "contracts/periphery/overview", + "contracts/periphery/refund-request", + "contracts/periphery/refund-request-evidence", + "contracts/periphery/receiver-refund-collector" + ] + }, { "group": "Conditions", "pages": [ diff --git a/index.mdx b/index.mdx index dd471df..d02c959 100644 --- a/index.mdx +++ b/index.mdx @@ -71,20 +71,33 @@ sequenceDiagram ## Architecture -x402r consists of three main components: +x402r consists of these core components: | Component | Purpose | |-----------|---------| -| **PaymentOperator** | Manages payment authorization, release, charge, and refunds | +| **PaymentOperator** | Manages payment authorization, release, charge, and refunds with pluggable conditions | +| **AuthCaptureEscrow** | Holds ERC-20 tokens during the payment lifecycle (from commerce-payments) | +| **Conditions & Recorders** | Pluggable authorization checks (before action) and state updates (after action) | +| **EscrowPeriod & Freeze** | Time-based release and freeze policies for buyer protection | | **RefundRequest** | Handles refund request lifecycle and approvals | -| **EscrowPeriod** | Tracks escrow timing for time-based release conditions | + +All protocol contracts use unified CREATE3 addresses — same address on every supported chain. ## Supported Networks | Network | Chain ID | Status | |---------|----------|--------| -| Base Sepolia | 84532 | Supported | -| Base Mainnet | 8453 | Supported | +| Base | 8453 | Supported | +| Ethereum | 1 | Supported | +| Polygon | 137 | Supported | +| Arbitrum One | 42161 | Supported | +| Optimism | 10 | Supported | +| Celo | 42220 | Supported | +| Avalanche C-Chain | 43114 | Supported | +| Monad | 143 | Supported | +| Linea | 59144 | Supported | +| Base Sepolia | 84532 | Testnet | +| Ethereum Sepolia | 11155111 | Testnet | ## Resources diff --git a/roadmap.mdx b/roadmap.mdx index 3da3270..290045a 100644 --- a/roadmap.mdx +++ b/roadmap.mdx @@ -19,9 +19,15 @@ icon: "map" - PaymentOperator with pluggable conditions and recorders - Partial refund support (partial void) - EscrowPeriod and Freeze contracts -- Factory pattern with CREATE2 deterministic deployment -- ArbiterRegistry for on-chain arbiter discovery -- Deployed on Base Sepolia and Base Mainnet +- Factory pattern with unified CREATE3 deterministic deployment +- RefundRequestEvidence for on-chain evidence submission +- Deployed across 11 chains with unified CREATE3 addresses (same address everywhere) + +### Escrow Scheme Spec (In Progress) + +- [Proposal submitted to coinbase/x402 (Issue #1011)](https://github.com/coinbase/x402/issues/1011) +- [Spec PR submitted (PR #1425)](https://github.com/coinbase/x402/pull/1425) +- Reference implementation: [x402r-scheme](https://github.com/BackTrackCo/x402r-scheme) ### Developer Experience (In Progress) @@ -89,20 +95,24 @@ icon: "map" ## Contract Status -| Contract | Base Sepolia | Base Mainnet | -|----------|-------------|--------------| -| AuthCaptureEscrow | Deployed | Deployed | -| ERC3009PaymentCollector | Deployed | Deployed | -| PaymentOperatorFactory | Deployed | Deployed | -| EscrowPeriodFactory | Deployed | Deployed | -| FreezeFactory | Deployed | Deployed | -| RefundRequest | Deployed | Deployed | -| ArbiterRegistry | Deployed | Deployed | -| ProtocolFeeConfig | Deployed | Deployed | -| Condition singletons | Deployed | Deployed | +All contracts use **unified CREATE3 addresses** — same address on every supported chain (11 chains: Base, Ethereum, Polygon, Arbitrum, Optimism, Celo, Avalanche, Monad, Linea, Base Sepolia, Ethereum Sepolia). + +| Contract | Status | +|----------|--------| +| AuthCaptureEscrow | Deployed | +| ERC3009PaymentCollector | Deployed | +| PaymentOperatorFactory | Deployed | +| EscrowPeriodFactory | Deployed | +| FreezeFactory | Deployed | +| StaticFeeCalculatorFactory | Deployed | +| All condition/combinator factories | Deployed | +| ProtocolFeeConfig | Deployed | +| RefundRequestEvidence | Deployed | +| ReceiverRefundCollector | Deployed | +| Condition singletons (Payer, Receiver, AlwaysTrue) | Deployed | -All contract addresses are available in `@x402r/core` via `getNetworkConfig()`. See the [Installation](/sdk/installation) page for details. +All contract addresses are available in `@x402r/core` via `getChainConfig(chainId)`. See the [Installation](/sdk/installation) page for details. ## Get Involved diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx index 6a1aba8..696479e 100644 --- a/sdk/arbiter/decision-submission.mdx +++ b/sdk/arbiter/decision-submission.mdx @@ -248,7 +248,7 @@ flowchart TD Register as an arbiter for on-chain discovery. - + RefundRequest contract and state machine details. diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index fd48601..e5bdd20 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -259,7 +259,7 @@ flowchart TD Watch for real-time payment and refund events. - + Understand the underlying PaymentOperator contract methods. diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index bc6e155..4929d3f 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -235,7 +235,7 @@ flowchart TD Mark payment options as refundable with your operator. - + Understand the underlying PaymentOperator contract methods. diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index c0b43fe..02e8b31 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -316,7 +316,7 @@ sequenceDiagram Release funds, charge, and query escrow state. - + RefundRequest contract details and state machine. diff --git a/x402-integration/comparison.mdx b/x402-integration/comparison.mdx index 907c0ca..d6b34c8 100644 --- a/x402-integration/comparison.mdx +++ b/x402-integration/comparison.mdx @@ -278,12 +278,16 @@ You can support **both schemes** and let clients choose: { "scheme": "escrow", "network": "eip155:8453", - "maxAmountRequired": "10000000", + "amount": "10000000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0xReceiver...", "extra": { - "escrowAddress": "0xEscrow...", - "operatorAddress": "0xOperator..." + "name": "USDC", + "version": "2", + "escrowAddress": "0xe050bB89eD43BB02d71343063824614A7fb80B77", + "operatorAddress": "0xOperator...", + "tokenCollector": "0xcE66Ab399EDA513BD12760b6427C87D6602344a7", + "settlementMethod": "authorize" } } ] diff --git a/x402-integration/escrow-scheme.mdx b/x402-integration/escrow-scheme.mdx index f5e80cb..70a3c0e 100644 --- a/x402-integration/escrow-scheme.mdx +++ b/x402-integration/escrow-scheme.mdx @@ -6,34 +6,71 @@ icon: "file-contract" ## Overview -The **escrow scheme** for x402 v2 leverages the [Base Commerce Payments Protocol](https://github.com/base/commerce-payments) to enable secure, conditional fund handling using the auth/capture pattern. +The **escrow scheme** for x402 v2 uses the [Commerce Payments Protocol](https://github.com/base/commerce-payments) contract stack to enable secure, conditional fund handling. The client signs a single ERC-3009 authorization. The facilitator submits it to an operator, which handles token collection, escrow locking, and fee distribution in one transaction. -This proposal is based on [Agentokratia's escrow proposal (#834)](https://github.com/coinbase/x402/issues/834) and extends it to support arbitrary operator implementations using audited on-chain escrow contracts. +This spec is based on the [escrow scheme proposal (Issue #1011)](https://github.com/coinbase/x402/issues/1011) and the [spec PR (#1425)](https://github.com/coinbase/x402/pull/1425). It uses audited on-chain escrow contracts from the Commerce Payments Protocol. -## Auth/Capture Pattern +## Settlement Methods -The escrow scheme provides four primitives: +The scheme supports two settlement paths: + +| Method | Behavior | +|:-------|:---------| +| `authorize` (default) | Funds held in escrow. Can be captured, voided, reclaimed, or refunded. | +| `charge` | Funds sent directly to receiver. Refundable post-settlement. | + +### Authorize (Default) + +``` +AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) +``` - Lock funds in escrow at request time. Client signs ERC-3009 authorization, facilitator calls `escrow.authorize()` to pull tokens into escrow contract. + Client authorization is submitted — funds locked in escrow via `operator.authorize()`. The operator calls the token collector to execute `receiveWithAuthorization` with the client's ERC-3009 signature, then routes funds into the escrow contract. - - Release earned amount to receiver. Operator calls `escrow.release()` based on configured conditions (time elapsed, work verified, etc.). + + Server returns the resource (HTTP 200). - - Return unused funds to payer. Operator calls `escrow.void()` for full refunds during escrow period. + + The operator can capture (release funds to receiver via `operator.release()`) or void (return escrowed funds to client). Capture conditions are configurable per operator (time-locked, arbiter-approved, etc.). - Payer safety valve if operator disappears. After `authorizationExpiry`, payer can reclaim funds directly without operator approval. + If `authorizationExpiry` passes without capture, the client can reclaim funds directly from escrow without operator approval. + + + + After capture, the operator can refund within the `refundExpiry` window via `operator.refundPostEscrow()`. +### Charge + +``` +CHARGE -> RESOURCE DELIVERED -> (REFUND) +``` + + + + Client authorization is submitted — funds sent directly to receiver via `operator.charge()`. No escrow hold. + + + + Server returns the resource (HTTP 200). + + + + The operator can refund within the `refundExpiry` window via `operator.refundPostEscrow()`. + + + +No capture, void, or reclaim — funds are never held in escrow. + ## Visual Flow ### Exact Payment (Immediate Settlement) @@ -48,7 +85,7 @@ sequenceDiagram Server->>Receiver: 2. Immediate Transfer Server->>Client: 3. Deliver Resource - Note over Client,Receiver: ⚠️ No recourse after payment - Payment is final + Note over Client,Receiver: No recourse after payment - Payment is final ``` ### Escrow Payment (Deferred Settlement) @@ -56,19 +93,35 @@ sequenceDiagram ```mermaid sequenceDiagram participant Client - participant Escrow as Escrow Contract - participant Operator participant Server + participant Facilitator + participant Operator + participant Escrow participant Receiver - Client->>Escrow: 1. Authorize (Lock funds) - Server->>Client: 2. Deliver Resource - Note over Escrow,Operator: 3. Hold until conditions met - Operator->>Escrow: 4. release() - Escrow->>Receiver: 5. Transfer funds + Client->>Server: GET /resource + Server-->>Client: 402 PaymentRequired + Note over Client: Signs ERC-3009 authorization - Note over Client,Escrow: Alt: Client can reclaim after expiry - Note over Client,Receiver: ✓ Protected until conditions met - Funds held safely, refundable + Client->>Server: PaymentPayload with signature + Server->>Facilitator: verify + settle + Facilitator->>Operator: authorize(paymentInfo, amount, tokenCollector, signature) + Operator->>Escrow: Lock funds in escrow + Facilitator-->>Server: Settlement confirmed + Server-->>Client: 200 OK + resource + + Note over Operator,Escrow: Later: operator releases based on conditions + + alt Successful completion + Operator->>Escrow: release(paymentInfo, amount) + Escrow->>Receiver: Transfer funds (minus fees) + else Void (full refund in escrow) + Operator->>Escrow: void(paymentInfo) + Escrow->>Client: Return to payer + else Authorization expired + Client->>Escrow: reclaim() + Escrow->>Client: Return to payer (no operator needed) + end ``` ### Key Differences @@ -78,7 +131,8 @@ sequenceDiagram | **Settlement** | Immediate on request | Deferred until conditions met | | **Payer Protection** | None (payment final) | Refundable until capture | | **Resource Delivery** | After payment clears | Immediately after authorization | -| **Recourse** | No recourse | Can reclaim after expiry | +| **Recourse** | No recourse | Reclaim after expiry, refund via operator | +| **Fee System** | None | Configurable (min/max bounds, client-signed) | | **Use Case** | Trusted, low-value, instant | High-value, variable cost, disputes | ## Operator Flexibility @@ -95,7 +149,7 @@ The **operator** is the key abstraction. Different implementations enable differ ## Message Format -### PaymentRequired (402 Response) +### PaymentRequirements (402 Response) Server sends this to request payment: @@ -105,23 +159,20 @@ Server sends this to request payment: "accepts": [{ "scheme": "escrow", "network": "eip155:8453", - "amount": "10000000", + "amount": "1000000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "payTo": "0xReceiver...", + "payTo": "0xReceiverAddress", + "maxTimeoutSeconds": 60, "extra": { "name": "USDC", "version": "2", - "escrowAddress": "0xEscrow...", - "operatorAddress": "0xOperator...", - "authorizeAddress": "0xOperator...", - "tokenCollector": "0xCollector...", - "minDeposit": "5000000", - "maxDeposit": "100000000", - "authorizationExpirySeconds": 259200, - "refundExpirySeconds": 31536000, + "escrowAddress": "0xe050bB89eD43BB02d71343063824614A7fb80B77", + "operatorAddress": "0xOperatorAddress", + "tokenCollector": "0xcE66Ab399EDA513BD12760b6427C87D6602344a7", + "settlementMethod": "authorize", "minFeeBps": 0, - "maxFeeBps": 0, - "feeReceiver": "0xFeeReceiver..." + "maxFeeBps": 1000, + "feeReceiver": "0xOperatorAddress" } }] } @@ -134,28 +185,41 @@ Client sends this with signed authorization: ```json { "x402Version": 2, - "scheme": "escrow", + "resource": { + "url": "https://api.example.com/resource", + "method": "GET" + }, + "accepted": { + "scheme": "escrow", + "network": "eip155:8453", + "amount": "1000000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0xReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { "..." } + }, "payload": { "authorization": { - "from": "0xPayer...", - "to": "0xCollector...", - "value": "10000000", + "from": "0xPayerAddress", + "to": "0xcE66Ab399EDA513BD12760b6427C87D6602344a7", + "value": "1000000", "validAfter": "0", - "validBefore": "1734567890", - "nonce": "0x..." + "validBefore": "1740672154", + "nonce": "0xf374...3480" }, - "signature": "0x...", + "signature": "0x2d6a...571c", "paymentInfo": { - "operator": "0xOperator...", - "receiver": "0xReceiver...", + "operator": "0xOperatorAddress", + "receiver": "0xReceiverAddress", "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "maxAmount": "10000000", + "maxAmount": "1000000", + "preApprovalExpiry": 1740672154, "authorizationExpiry": 4294967295, "refundExpiry": 281474976710655, "minFeeBps": 0, - "maxFeeBps": 0, - "feeReceiver": "0xFeeReceiver...", - "salt": "0x..." + "maxFeeBps": 1000, + "feeReceiver": "0xOperatorAddress", + "salt": "0x0000...0001" } } } @@ -167,98 +231,124 @@ Client sends this with signed authorization: | Field | Type | Description | |-------|------|-------------| +| `name` | string | EIP-712 domain name for the token (e.g., `"USDC"`) | +| `version` | string | EIP-712 domain version (e.g., `"2"`) | | `escrowAddress` | address | AuthCaptureEscrow contract address on the specified network | -| `operatorAddress` | address | Address with capture/void authority (stored in PaymentInfo.operator) | -| `tokenCollector` | address | ERC-3009/Permit2 token collector contract address | +| `operatorAddress` | address | Operator contract address (stored in PaymentInfo.operator) | +| `tokenCollector` | address | ERC-3009 token collector contract address | ### Optional Extra Fields | Field | Type | Description | Default | |-------|------|-------------|---------| -| `authorizeAddress` | address | Where facilitator calls authorize() | `operatorAddress` if operator is a contract, `escrowAddress` if operator is an EOA | -| `minDeposit` | uint256 | Minimum accepted deposit amount (wei) | No minimum | -| `maxDeposit` | uint256 | Maximum accepted deposit amount (wei) | No maximum | -| `authorizationExpirySeconds` | uint32 | Seconds until payer can reclaim funds | `type(uint32).max` (effectively never) | -| `refundExpirySeconds` | uint48 | Seconds until refunds are no longer allowed | `type(uint48).max` (effectively never) | +| `settlementMethod` | `"authorize"` \| `"charge"` | Settlement path | `"authorize"` | | `minFeeBps` | uint16 | Minimum fee in basis points | `0` | | `maxFeeBps` | uint16 | Maximum fee in basis points | `0` | -| `feeReceiver` | address | Address receiving fees | `operatorAddress` | +| `feeReceiver` | address | Address receiving fees | `address(0)` (flexible) | +| `preApprovalExpirySeconds` | uint48 | ERC-3009 signature validity / pre-approval deadline (seconds from now) | `type(uint48).max` | +| `authorizationExpirySeconds` | uint48 | Deadline for capturing escrowed funds (seconds from now) | `type(uint48).max` | +| `refundExpirySeconds` | uint48 | Deadline for refund requests (seconds from now) | `type(uint48).max` | -**Fee Configuration:** Fees are enforced on-chain in the PaymentInfo struct. The operator contract cannot charge more than `maxFeeBps` or less than `minFeeBps`. +**Fee Configuration:** Fees are enforced on-chain in the PaymentInfo struct. The operator contract cannot charge more than `maxFeeBps` or less than `minFeeBps`. If `feeReceiver` is set, the actual fee recipient at capture/charge must match. -## Authorization vs Capture - -Understanding the two-phase process: - -### Phase 1: Authorization (HTTP Request) - -```typescript -// Client signs ERC-3009 authorization -const authorization = { - from: payerAddress, - to: tokenCollectorAddress, - value: maxAmount, - validAfter: 0, - validBefore: deadline, - nonce: randomNonce() -}; - -const signature = await signERC3009(authorization); - -// Send to server in PaymentPayload -await fetch('/resource', { - headers: { - 'X-Payment': JSON.stringify({ - x402Version: 2, - scheme: 'escrow', - payload: { authorization, signature, paymentInfo } - }) - } -}); +## Nonce Derivation + +The ERC-3009 nonce is deterministically derived from the payment parameters: -// Server calls facilitator to settle -// Facilitator calls escrow.authorize() on-chain -// Funds locked in escrow contract ``` +nonce = keccak256(abi.encode(chainId, escrowAddress, paymentInfoHash)) +``` + +This ties the off-chain signature to the specific chain, escrow contract, and payment terms — preventing cross-chain or cross-contract replay. The nonce is consumed on-chain at settlement. + +## Verification Logic + +The facilitator performs these checks in order: + +1. **Type guard** — Verify `payload` contains `authorization`, `signature`, and `paymentInfo` fields +2. **Scheme match** — Verify `scheme === "escrow"` +3. **Network match** — Verify network format is `eip155:` and matches between requirements and payload +4. **Extra validation** — Verify `extra` contains required fields (`escrowAddress`, `operatorAddress`, `tokenCollector`) +5. **Time window** — Verify `validBefore > now + 6s` (not expired) and `validAfter <= now` (active) +6. **ERC-3009 signature** — Recover signer from EIP-712 typed data (`ReceiveWithAuthorization` primary type) and verify matches `authorization.from` +7. **Amount** — Verify `authorization.value >= requirements.amount` +8. **Recipient match** — Verify `authorization.to === extra.tokenCollector` +9. **Token match** — Verify `paymentInfo.token === requirements.asset` +10. **Receiver match** — Verify `paymentInfo.receiver === requirements.payTo` +11. **Simulate** — Call `operator.authorize(...)` or `operator.charge(...)` via `eth_call` to verify success -### Phase 2: Capture (After Work) - -```typescript -// Later: operator decides to release funds -// This could be: -// - Immediate (for exact-like behavior) -// - After time period (escrow period elapsed) -// - After verification (arbiter approved) -// - Batch settlement (multiple captures from one authorization) - -await operator.release(paymentId); -// or -await operator.charge(paymentId, partialAmount); -// or -await operator.void(paymentId); // full refund +### EIP-6492 Support + +For smart wallet clients, the signature may be EIP-6492 wrapped (containing deployment bytecode). The facilitator extracts the inner ECDSA signature for verification. The on-chain `ERC6492SignatureHandler` in the token collector handles wallet deployment during settlement. + +## Settlement Logic + +Settlement is performed by the facilitator calling the operator: + +1. **Re-verify** the payload (catch expired/invalid payloads before spending gas) +2. **Determine function** — `settlementMethod === "charge" ? "charge" : "authorize"` +3. **Call operator** — `operator.(paymentInfo, amount, tokenCollector, collectorData)` +4. **Wait for receipt** — Confirm transaction success (60s timeout) +5. **Return result** — Transaction hash, network, and payer address + +The operator handles: + +- Calling the token collector to execute `receiveWithAuthorization` with the client's ERC-3009 signature +- Routing funds to escrow (authorize) or directly to receiver (charge) +- Validating fee bounds against the client-signed `PaymentInfo` + +## PaymentInfo Struct + +This struct is signed by the client and validated on-chain: + +```solidity +struct PaymentInfo { + address operator; // Operator address + address payer; // Client wallet (authorization.from) + address receiver; // Fund recipient (payTo) + address token; // ERC-20 token address + uint120 maxAmount; // Maximum authorized amount + uint48 preApprovalExpiry; // ERC-3009 validBefore / pre-approval deadline + uint48 authorizationExpiry;// Capture deadline (authorize path only) + uint48 refundExpiry; // Refund request deadline + uint16 minFeeBps; // Minimum acceptable fee (basis points) + uint16 maxFeeBps; // Maximum acceptable fee (basis points) + address feeReceiver; // Fee recipient (address(0) = flexible) + uint256 salt; // Client-provided entropy +} ``` +### Expiry Ordering + +The contract enforces: `preApprovalExpiry <= authorizationExpiry <= refundExpiry` + +| Expiry | Enforced At | Effect | +|:-------|:------------|:-------| +| `preApprovalExpiry` | `authorize()` / `charge()` | Blocks settlement after this time | +| `authorizationExpiry` | `capture()` | Blocks capture; enables `reclaim()` | +| `refundExpiry` | `refund()` | Blocks refund requests | + ## Safety Guarantees The escrow contract enforces invariants on-chain: - Operator cannot capture more than authorized `maxAmount`. Attempting to exceed the limit reverts the transaction. + Settlement amount is capped by client-signed `maxAmount`. Attempting to exceed the limit reverts the transaction. - - Funds are locked at authorization. Payer cannot use the same tokens elsewhere until void/reclaim. + + Each payment has a unique nonce derived from `(chainId, escrowAddress, paymentInfoHash)`. The nonce is consumed on-chain at settlement. - If `authorizationExpiry` is set, payer can reclaim funds after expiry without operator approval. + After `authorizationExpiry`, payer can reclaim escrowed funds directly without operator approval. - Min/max fee bounds in PaymentInfo are enforced on-chain. Operator must respect these limits. + Min/max fee bounds in PaymentInfo are client-signed and enforced on-chain. The operator must respect these limits. @@ -266,66 +356,33 @@ The escrow contract enforces invariants on-chain: **Operator Trust Required:** The operator contract controls when and how much to release. Choose operators carefully and understand their release conditions. See [Operators](/contracts/overview#payment-operator) for details. -## Sequence Diagram - -```mermaid -sequenceDiagram - participant Client - participant Server - participant Facilitator - participant TokenCollector - participant Escrow - participant Operator - - Client->>Server: GET /resource - Server-->>Client: 402 PaymentRequired - Note over Server,Client: includes escrowAddress, operatorAddress, etc. - - Note over Client: User signs ERC-3009 authorization - Client->>Client: signERC3009(authorization) - - Client->>Server: PaymentPayload with signature - Server->>Facilitator: verify(signature, paymentInfo) - Facilitator->>Facilitator: Validate signature & amounts - - Facilitator->>TokenCollector: transferWithAuthorization() - TokenCollector->>Escrow: Transfer tokens to escrow - Note over Escrow: Tokens locked in escrow - - Facilitator->>Escrow: authorize(paymentId, ...) - Note over Escrow: Payment state = InEscrow - - Facilitator-->>Server: Settlement confirmed - Server-->>Client: 200 OK + resource - - Note over Operator,Escrow: Later: operator releases funds based on conditions - - alt Successful completion - Operator->>Escrow: release(paymentId) - Escrow->>Operator: Transfer to receiver - Note over Escrow: Payment state = Released - else Refund needed - Operator->>Escrow: void(paymentId) - Escrow->>Client: Return to payer - Note over Escrow: Payment state = Settled - else Authorization expired - Client->>Escrow: reclaim(paymentId) - Escrow->>Client: Return to payer - Note over Escrow: Payer reclaims without operator - end -``` - -## Self-Hosted Facilitator - -x402r ships a self-hosted facilitator service at `x402r-sdk/examples/facilitator/` that implements x402's facilitator protocol for escrow payments. This service: - -1. Exposes a `/supported` endpoint with escrow scheme configuration (operator, escrow, and token collector addresses) -2. Verifies payment signatures via `/verify` -3. Settles payments on-chain via `/settle` by calling `authorize()` on the PaymentOperator - -Merchant servers use x402's standard `paymentMiddleware` with an `HTTPFacilitatorClient` pointing to this service, so they don't need to handle blockchain interactions directly. - -See [Examples](/sdk/examples) for the complete setup guide. +## Error Codes + +### Verification Errors + +| Error Code | Description | +|:-----------|:------------| +| `invalid_payload_format` | Payload missing `authorization`, `signature`, or `paymentInfo` | +| `unsupported_scheme` | Scheme is not `escrow` | +| `network_mismatch` | Payload network does not match requirements | +| `invalid_network` | Network format is not `eip155:` | +| `invalid_escrow_extra` | Missing required extra fields (`escrowAddress`, `operatorAddress`, `tokenCollector`) | +| `authorization_expired` | `validBefore <= now + 6s` | +| `authorization_not_yet_valid` | `validAfter > now` | +| `invalid_escrow_signature` | ERC-3009 signature verification failed | +| `amount_mismatch` | `authorization.value !== requirements.amount` | +| `token_collector_mismatch` | `authorization.to !== extra.tokenCollector` | +| `token_mismatch` | `paymentInfo.token !== requirements.asset` | +| `receiver_mismatch` | `paymentInfo.receiver !== requirements.payTo` | +| `insufficient_balance` | Payer balance is less than required amount | +| `simulation_failed` | Settlement simulation via `eth_call` failed | + +### Settlement Errors + +| Error Code | Description | +|:-----------|:------------| +| `verification_failed` | Re-verification before settlement failed | +| `transaction_reverted` | On-chain transaction reverted after confirmation | ## vs Exact Scheme @@ -355,8 +412,11 @@ See [Comparison](/x402-integration/comparison) for detailed trade-offs. ## References -- [Base Commerce Payments Protocol](https://blog.base.dev/commerce-payments-protocol) +- [Escrow Scheme Proposal (Issue #1011)](https://github.com/coinbase/x402/issues/1011) +- [Escrow Scheme Specification (PR #1425)](https://github.com/coinbase/x402/pull/1425) +- [Commerce Payments Protocol](https://blog.base.dev/commerce-payments-protocol) - [AuthCaptureEscrow Contract](https://github.com/base/commerce-payments) - [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) - [x402r Operator Contracts](https://github.com/BackTrackCo/x402r-contracts) +- [x402r Escrow Scheme](https://github.com/BackTrackCo/x402r-scheme) - [x402 Protocol](https://github.com/coinbase/x402) diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index 38f81c1..b97f466 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -118,23 +118,22 @@ sequenceDiagram participant Client participant Server participant Facilitator - participant Escrow participant Operator + participant Escrow Client->>Server: GET /resource Server-->>Client: 402 Payment Required Note over Client: Signs ERC-3009 authorization Client->>Server: Payment payload Server->>Facilitator: Verify & settle - Facilitator->>Escrow: authorize(paymentId, amount) - Note over Escrow: Funds locked + Facilitator->>Operator: authorize(paymentInfo, amount, tokenCollector, signature) + Operator->>Escrow: Lock funds in escrow Facilitator-->>Server: Settlement confirmed Server-->>Client: 200 OK + resource - Note over Server,Operator: Later: capture or void - Server->>Operator: capture(paymentId) - Operator->>Escrow: release(paymentId) - Note over Escrow: Funds released to receiver + Note over Operator,Escrow: Later: capture or void + Operator->>Escrow: release(paymentInfo, amount) + Note over Escrow: Funds released to receiver (minus fees) ``` ## Use Cases