Skip to main content

Overview

The escrow scheme for x402 v2 uses the Commerce Payments Protocol 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 spec is based on the escrow scheme proposal (Issue #1011) and the spec PR (#1425). It uses audited on-chain escrow contracts from the Commerce Payments Protocol.

Settlement Methods

The scheme supports two settlement paths:
MethodBehavior
authorize (default)Funds held in escrow. Can be captured, voided, reclaimed, or refunded.
chargeFunds sent directly to receiver. Refundable post-settlement.

Authorize (Default)

AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND)
1

Authorize

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.
2

Resource Delivered

Server returns the resource (HTTP 200).
3

Capture or Void

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.).
4

Reclaim

If authorizationExpiry passes without capture, the client can reclaim funds directly from escrow without operator approval.
5

Refund (Optional)

After capture, the operator can refund within the refundExpiry window via operator.refundPostEscrow().

Charge

CHARGE -> RESOURCE DELIVERED -> (REFUND)
1

Charge

Client authorization is submitted — funds sent directly to receiver via operator.charge(). No escrow hold.
2

Resource Delivered

Server returns the resource (HTTP 200).
3

Refund (Optional)

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)

Escrow Payment (Deferred Settlement)

Key Differences

AspectExactEscrow
SettlementImmediate on requestDeferred until conditions met
Payer ProtectionNone (payment final)Refundable until capture
Resource DeliveryAfter payment clearsImmediately after authorization
RecourseNo recourseReclaim after expiry, refund via operator
Fee SystemNoneConfigurable (min/max bounds, client-signed)
Use CaseTrusted, low-value, instantHigh-value, variable cost, disputes

Operator Flexibility

The operator is the key abstraction. Different implementations enable different payment patterns:
Use CaseOperator Behavior
Session billingTrack usage off-chain, capture periodically
Time-locked escrowRelease after period expires
Dispute resolutionArbiter decides release vs refund
Immediate (exact-like)Use charge() for instant settlement
Streaming paymentsTime-proportional captures

Message Format

PaymentRequirements (402 Response)

Server sends this to request payment:
{
  "x402Version": 2,
  "accepts": [{
    "scheme": "escrow",
    "network": "eip155:8453",
    "amount": "1000000",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "payTo": "0xReceiverAddress",
    "maxTimeoutSeconds": 60,
    "extra": {
      "name": "USDC",
      "version": "2",
      "escrowAddress": "0xe050bB89eD43BB02d71343063824614A7fb80B77",
      "operatorAddress": "0xOperatorAddress",
      "tokenCollector": "0xcE66Ab399EDA513BD12760b6427C87D6602344a7",
      "settlementMethod": "authorize",
      "minFeeBps": 0,
      "maxFeeBps": 1000,
      "feeReceiver": "0xOperatorAddress"
    }
  }]
}

PaymentPayload (Client Response)

Client sends this with signed authorization:
{
  "x402Version": 2,
  "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": "0xPayerAddress",
      "to": "0xcE66Ab399EDA513BD12760b6427C87D6602344a7",
      "value": "1000000",
      "validAfter": "0",
      "validBefore": "1740672154",
      "nonce": "0xf374...3480"
    },
    "signature": "0x2d6a...571c",
    "paymentInfo": {
      "operator": "0xOperatorAddress",
      "receiver": "0xReceiverAddress",
      "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "maxAmount": "1000000",
      "preApprovalExpiry": 1740672154,
      "authorizationExpiry": 4294967295,
      "refundExpiry": 281474976710655,
      "minFeeBps": 0,
      "maxFeeBps": 1000,
      "feeReceiver": "0xOperatorAddress",
      "salt": "0x0000...0001"
    }
  }
}

Field Reference

Required Extra Fields

FieldTypeDescription
namestringEIP-712 domain name for the token (e.g., "USDC")
versionstringEIP-712 domain version (e.g., "2")
escrowAddressaddressAuthCaptureEscrow contract address on the specified network
operatorAddressaddressOperator contract address (stored in PaymentInfo.operator)
tokenCollectoraddressERC-3009 token collector contract address

Optional Extra Fields

FieldTypeDescriptionDefault
settlementMethod"authorize" | "charge"Settlement path"authorize"
minFeeBpsuint16Minimum fee in basis points0
maxFeeBpsuint16Maximum fee in basis points0
feeReceiveraddressAddress receiving feesaddress(0) (flexible)
preApprovalExpirySecondsuint48ERC-3009 signature validity / pre-approval deadline (seconds from now)type(uint48).max
authorizationExpirySecondsuint48Deadline for capturing escrowed funds (seconds from now)type(uint48).max
refundExpirySecondsuint48Deadline 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. If feeReceiver is set, the actual fee recipient at capture/charge must match.

Nonce Derivation

The ERC-3009 nonce is deterministically derived from the payment parameters:
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:<chainId> 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

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 functionsettlementMethod === "charge" ? "charge" : "authorize"
  3. Call operatoroperator.<fn>(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 is the on-chain Solidity struct. The payer field is not included in the JSON payload — it is derived from authorization.from at settlement time.
struct PaymentInfo {
    address operator;           // Operator address
    address payer;              // Derived from authorization.from (not in payload)
    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
ExpiryEnforced AtEffect
preApprovalExpiryauthorize() / charge()Blocks settlement after this time
authorizationExpirycapture()Blocks capture; enables reclaim()
refundExpiryrefund()Blocks refund requests

Safety Guarantees

The escrow contract enforces invariants on-chain:

No Overcharging

Settlement amount is capped by client-signed maxAmount. Attempting to exceed the limit reverts the transaction.

Replay Prevention

Each payment has a unique nonce derived from (chainId, escrowAddress, paymentInfoHash). The nonce is consumed on-chain at settlement.

Payer Reclaim

After authorizationExpiry, payer can reclaim escrowed funds directly without operator approval.

Fee Bounds

Min/max fee bounds in PaymentInfo are client-signed and enforced on-chain. The operator must respect these limits.
Operator Trust Required: The operator contract controls when and how much to release. Choose operators carefully and understand their release conditions. See Operators for details.

Error Codes

Verification Errors

Error CodeDescription
invalid_payload_formatPayload missing authorization, signature, or paymentInfo
unsupported_schemeScheme is not escrow
network_mismatchPayload network does not match requirements
invalid_networkNetwork format is not eip155:<chainId>
invalid_escrow_extraMissing required extra fields (escrowAddress, operatorAddress, tokenCollector)
authorization_expiredvalidBefore <= now + 6s
authorization_not_yet_validvalidAfter > now
invalid_escrow_signatureERC-3009 signature verification failed
amount_mismatchauthorization.value !== requirements.amount
token_collector_mismatchauthorization.to !== extra.tokenCollector
token_mismatchpaymentInfo.token !== requirements.asset
receiver_mismatchpaymentInfo.receiver !== requirements.payTo
insufficient_balancePayer balance is less than required amount
simulation_failedSettlement simulation via eth_call failed

Settlement Errors

Error CodeDescription
verification_failedRe-verification before settlement failed
transaction_revertedOn-chain transaction reverted after confirmation

vs Exact Scheme

The escrow scheme adds an authorization step before settlement. For simple immediate payments where trust is not a concern, the exact scheme remains more efficient. See Comparison for detailed trade-offs.

Next Steps

Overview

Understand why escrow is needed for HTTP payments.

Comparison

Compare escrow vs exact schemes in detail.

Smart Contracts

Learn about escrow and operator contracts.

SDK Installation

Build your first escrow-based payment flow.

References