-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add authCapture scheme specification
#1425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
CarsonRoscoe
merged 5 commits into
x402-foundation:main
from
BackTrackCo:feature/escrow-scheme-spec
May 13, 2026
+382
−0
Merged
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e20a61f
Add escrow scheme specification
A1igator 374c4a0
Address review feedback on escrow scheme specs
A1igator cf556b3
Rename scheme to commerce and align spec with implementation
A1igator 3f43b52
spec: rename escrow/commerce scheme to authCapture, add EVM payload s…
A1igator 8f070c8
spec: use canonical base/commerce-payments addresses
A1igator File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # Scheme: `escrow` | ||
|
|
||
| ## Summary | ||
|
|
||
| `escrow` is a payment scheme where funds can be held and settled later. The client authorizes a maximum amount, and the facilitator submits it — either locking funds in escrow for later settlement (authorize) or sending them directly to the receiver with refund capability (charge). | ||
|
|
||
| The **operator** is the entity that routes funds and manages the payment lifecycle (capture, refund, void). It may be the facilitator itself, a separate authorized account, or a smart contract — depending on the network and implementation. | ||
|
|
||
| Unlike `exact`, which has no built-in mechanism for returning funds, `escrow` supports returning funds to the client through void, refund, and reclaim. | ||
|
|
||
| ## Example Use Cases | ||
|
|
||
| - Refundable payments with buyer protection | ||
| - Delayed delivery where the client needs recourse if the service is unsatisfactory | ||
| - Subscription or session billing with periodic captures against a single authorization | ||
|
|
||
| ## Settlement Methods | ||
|
|
||
| The scheme supports two settlement paths: | ||
|
|
||
| | Method | Behavior | | ||
| | :---------- | :--------------------------------------------------------------------- | | ||
| | `authorize` | Funds held in escrow. Can be captured, refunded, voided, or reclaimed. | | ||
| | `charge` | Funds 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 | ||
| 2. **Resource delivered**: Server returns the resource (HTTP 200) | ||
| 3. **Capture or void**: The operator can capture (finalize funds to the receiver) or void (release escrowed funds back to client). | ||
| 4. **Reclaim**: If the capture deadline passes without action, the client can reclaim directly. | ||
| 5. **Refund**: After capture, the operator can refund within the refund window. | ||
|
|
||
| ### Charge | ||
|
|
||
| ``` | ||
| CHARGE → RESOURCE DELIVERED → (REFUND) | ||
| ``` | ||
|
|
||
| 1. **Charge**: Client authorization is submitted — funds sent directly to receiver | ||
| 2. **Resource delivered**: Server returns the resource (HTTP 200) | ||
| 3. **Refund**: The operator can refund within the refund window. | ||
|
|
||
| No capture, void, or reclaim — funds are never held in escrow. | ||
|
|
||
| ## Core Properties | ||
|
|
||
| ### Fund Safety | ||
|
|
||
| - Cannot overcharge — settlement amount is capped by the client-signed maximum | ||
| - Authorize path: client can reclaim escrowed funds after the capture deadline if no action is taken | ||
| - Fee bounds are client-signed and enforced at settlement | ||
|
|
||
| ### Replay Prevention | ||
|
|
||
| - Each payment has a unique nonce derived from the payment parameters | ||
| - Nonce is consumed on-chain at settlement, preventing double-spend | ||
|
|
||
| ### Expiry Enforcement | ||
|
|
||
| Three ordered deadlines govern the payment lifecycle: | ||
|
|
||
| - **Authorization deadline**: Last moment to submit the client's authorization for settlement | ||
| - **Capture deadline**: Last moment to capture escrowed funds (authorize path); after this, the client can reclaim | ||
| - **Refund deadline**: Last moment to issue a refund on captured or charged payments | ||
|
|
||
| ## Relationship to `exact` | ||
|
|
||
| | Aspect | `exact` | `escrow` | | ||
| | :--------- | :----------------- | :--------------------------------------------------------------- | | ||
| | Settlement | Immediate transfer | Via escrow (authorize) or direct with refund capability (charge) | | ||
| | Refundable | No | Yes (both paths) | | ||
| | Fee system | None | Configurable (min/max bounds, client-signed) | | ||
|
|
||
| ## Appendix | ||
|
|
||
| Network-specific implementation details (contracts, signature formats, verification logic) are in per-network documents: `scheme_escrow_evm.md` (EVM). | ||
|
|
||
| ### References | ||
|
|
||
| - [Escrow Scheme Proposal — Agentokratia (Issue #834)](https://github.com/coinbase/x402/issues/834) | ||
| - [Escrow Scheme Proposal — x402r (Issue #1011)](https://github.com/coinbase/x402/issues/1011) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| # Scheme: `escrow` on `EVM` | ||
|
|
||
| ## Summary | ||
|
|
||
| The `escrow` scheme on EVM uses the [Commerce Payments Protocol](https://github.com/base/commerce-payments) contract stack: | ||
|
|
||
| - **Escrow** (`AuthCaptureEscrow`): Singleton — locks funds, enforces expiries, distributes on capture/refund | ||
| - **Operator**: Routes payments through escrow with fee management | ||
| - **Token Collector** (`ERC3009PaymentCollector`): Collects funds via `receiveWithAuthorization` signatures | ||
|
|
||
| The client signs a single ERC-3009 authorization. The facilitator submits it to the operator, which handles token collection, escrow locking, and fee distribution — all in one transaction. | ||
|
|
||
| The escrow scheme uses ERC-3009 (`receiveWithAuthorization`) exclusively. The commerce-payments token collector architecture supports pluggable collection methods; future collectors (e.g., Permit2) could be added via `assetTransferMethod` in `extra` without changing the scheme. | ||
|
|
||
| ## PaymentRequirements | ||
|
|
||
| Escrow-accepting servers advertise with scheme `escrow`: | ||
|
|
||
| ```json | ||
| { | ||
| "x402Version": 2, | ||
| "accepts": [ | ||
| { | ||
| "scheme": "escrow", | ||
| "network": "eip155:8453", | ||
| "amount": "1000000", | ||
| "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", | ||
| "payTo": "0xReceiverAddress", | ||
| "maxTimeoutSeconds": 60, | ||
| "extra": { | ||
| "name": "USDC", | ||
| "version": "2", | ||
| "escrowAddress": "0xEscrowAddress", | ||
| "operatorAddress": "0xOperatorAddress", | ||
| "tokenCollector": "0xCollectorAddress", | ||
| "settlementMethod": "authorize", | ||
| "minFeeBps": 0, | ||
| "maxFeeBps": 1000, | ||
| "feeReceiver": "0xOperatorAddress" | ||
|
A1igator marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ### `extra` Fields | ||
|
|
||
| | Field | Required | Type | Description | | ||
| | :--------------------------- | :------- | :------------------------ | :------------------------------------------------- | | ||
| | `name` | Yes | `string` | EIP-712 domain name for the token (e.g., `"USDC"`) | | ||
| | `version` | Yes | `string` | EIP-712 domain version (e.g., `"2"`) | | ||
| | `escrowAddress` | Yes | `address` | AuthCaptureEscrow contract address | | ||
| | `operatorAddress` | Yes | `address` | Operator address | | ||
| | `tokenCollector` | Yes | `address` | Token collector contract address | | ||
| | `settlementMethod` | No | `"authorize" \| "charge"` | Settlement path. Default: `"authorize"` | | ||
| | `minFeeBps` | No | `uint16` | Minimum fee in basis points. Default: `0` | | ||
| | `maxFeeBps` | No | `uint16` | Maximum fee in basis points. Default: `0` | | ||
| | `feeReceiver` | No | `address` | Fee recipient. Default: `address(0)` (no fees) | | ||
| | `preApprovalExpirySeconds` | No | `uint48` | ERC-3009 signature validity / pre-approval expiry | | ||
| | `authorizationExpirySeconds` | No | `uint48` | Deadline for capturing escrowed funds | | ||
| | `refundExpirySeconds` | No | `uint48` | Deadline for refund requests | | ||
|
|
||
| ## PaymentPayload | ||
|
|
||
| ```json | ||
| { | ||
| "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": { | ||
| "name": "USDC", | ||
| "version": "2", | ||
| "escrowAddress": "0xEscrowAddress", | ||
| "operatorAddress": "0xOperatorAddress", | ||
| "tokenCollector": "0xCollectorAddress", | ||
| "settlementMethod": "authorize", | ||
| "minFeeBps": 0, | ||
| "maxFeeBps": 1000, | ||
| "feeReceiver": "0xOperatorAddress" | ||
| } | ||
| }, | ||
| "payload": { | ||
| "authorization": { | ||
| "from": "0xPayerAddress", | ||
| "to": "0xCollectorAddress", | ||
| "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" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 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 escrow contract and payment terms, preventing cross-chain or cross-contract replay. | ||
|
|
||
| ## Verification Logic | ||
|
|
||
| The facilitator performs these checks in order: | ||
|
|
||
| 1. **Type guard**: Verify `payload` contains `authorization`, `signature`, and `paymentInfo` fields | ||
| 2. **Scheme match**: Verify `requirements.scheme === "escrow"` and `payload.accepted.scheme === "escrow"` | ||
| 3. **Network match**: Verify `payload.accepted.network === requirements.network` and format is `eip155:<chainId>` | ||
| 4. **Extra validation**: Verify `requirements.extra` contains required escrow 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 === requirements.extra.tokenCollector` | ||
| 9. **Token match**: Verify `paymentInfo.token === requirements.asset` | ||
| 10. **Receiver match**: Verify `paymentInfo.receiver === requirements.payTo` | ||
| 11. **Simulate** `operator.authorize(...)` or `operator.charge(...)` to ensure 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 function**: `settlementMethod === "charge" ? "charge" : "authorize"` | ||
| 3. **Call operator**: `operator.<functionName>(paymentInfo, amount, tokenCollector, collectorData)` | ||
| 4. **Wait for receipt**: Confirm transaction success with 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 signature (EIP-712 primary type: `ReceiveWithAuthorization`, not `TransferWithAuthorization`) | ||
| - Routing funds to escrow (authorize) or directly to receiver (charge) | ||
| - Validating fee bounds against the client-signed `PaymentInfo` | ||
|
|
||
| ## Error Codes | ||
|
|
||
| The escrow scheme uses the standard x402 error codes plus these scheme-specific 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:<chainId>` | | ||
| | `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 | | ||
|
|
||
| ## Appendix | ||
|
|
||
| ### 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 | | ||
|
|
||
| ### Fee System | ||
|
|
||
| Fees are enforced on-chain by the escrow contract: | ||
|
|
||
| - `minFeeBps` and `maxFeeBps` set by the client in `PaymentInfo` (0–10,000 bps) | ||
| - `feeBps` at capture/charge must fall within `[minFeeBps, maxFeeBps]` | ||
| - If `feeReceiver` is set in `PaymentInfo`, actual `feeReceiver` at capture/charge must match | ||
| - If `feeReceiver` is `address(0)`, the caller can specify any non-zero address | ||
| - Fee distribution: `feeAmount = amount * feeBps / 10000`, remainder goes to receiver | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auth-capture vs escrow?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to "commerce" as per Duke's suggestion above: #1425 (comment)