Conversation
Introduces the `escrow` scheme for x402, built on Base's Commerce Payments Protocol. Supports two settlement paths: authorize (funds held in escrow) and charge (direct to receiver), both refundable post-settlement. Refs: coinbase#834, coinbase#1011 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🟡 Heimdall Review Status
|
|
@A1igator is attempting to deploy a commit to the Coinbase Team on Vercel. A member of the Team first needs to authorize it. |
|
Thanks for the spec @A1igator - will be reviewing this shortly. |
|
Good catch on the spec/implementation mismatch for simulation — that's exactly what #1377 tracks. The There's a community contributor looking at a fix for |
Yes verify should include tx simulation, we have a PR up to add it to the exact implementation here: #1474 |
|
Thanks for putting this together @A1igator! As a general comment, |
| | `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"` | |
There was a problem hiding this comment.
Not sure about this branching based on metadata in extra. The payment flow seems to be quite different between the options, so maybe these should be 2 separate schemes?
There was a problem hiding this comment.
For exact we have assetTransferMethod = {3009, permit2} in extra but the payment flow is identical, assetTransferMethod is just an implementation detail
| 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` |
There was a problem hiding this comment.
| 7. **Amount**: Verify `authorization.value >= requirements.amount` | |
| 7. **Amount**: Verify `authorization.value === requirements.amount` |
| 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. **Token match**: Verify `paymentInfo.token === requirements.asset` | ||
| 9. **Receiver match**: Verify `paymentInfo.receiver === requirements.payTo` |
There was a problem hiding this comment.
Must also check payload.to === tokenCollector
| 2. **Charge**: Facilitator calls `charge()` on the operator — funds go directly to receiver | ||
| 3. **Resource delivered**: Server returns the resource (HTTP 200) | ||
|
|
||
| Post-settlement, the operator can refund within `refundExpiry` if needed. Unlike the authorize path, the payer cannot `reclaim()` — funds are already with the receiver. |
There was a problem hiding this comment.
If the funds are directly send to the receiver (server), how are refunds supposed to work? How is it decided if a refund is needed? Can a client request a refund? What happens in case of disputes? If the funds are never held in the escrow, why is this under a escrow scheme?
There was a problem hiding this comment.
Exact already guarantees that no payment is made on server failure. So I suppose whats covered here would be if the client is not happy with the delivered service which is inherently subjective
|
Could you please clarify who the operator is in the x402 context? Is it the facilitator or another separate entity? If its the facilitator, should it exposes additional endpoints: POST /capture, POST /void, POST /refund? |
|
Currently x402 is a stateless request-response protocol. Escrow introduces a long-lived lifecycle that extends beyond the original HTTP request. Could you clarify the state requirements for all the participants (client/server/facilitator)? Do we need to persists sth like a payment ID? The spec doesnt define the Can the server request a capture? How can the server correlate captured/refunded payments with the original request? |
Introduces the
escrowscheme for x402, built on Base's Commerce Payments Protocol. Supports two settlement paths: authorize (funds held in escrow) and charge (direct to receiver), both refundable post-settlement.Refs: #834, #1011
Description
Adds the escrow scheme specification as two files:
scheme_escrow.md— Scheme overview: settlement methods (authorize/charge), lifecycle, relationship toexact, security considerationsscheme_escrow_evm.md— EVM implementation: PaymentRequirements, PaymentPayload, verification logic, settlement logic, PaymentInfo struct, fee systemReuses audited Commerce Payments Protocol contracts (AuthCaptureEscrow, Operator, ERC3009PaymentCollector). Client signs a single ERC-3009
receiveWithAuthorization— same signature primitive asexact.Notes for reviewers:
Exact scheme spec/implementation mismatch: The
exactEVM spec (step 5) says to "simulatetoken.transferWithAuthorization(...)" but the current TypeScript implementation ineip3009.tsdoesn't actually simulate — it only does off-chain checks. Flagging in case the escrow spec's verification logic should align with actual practice vs stated spec.assetTransferMethodas future escrow extension: The escrow scheme currently uses ERC-3009 only, but the commerce-payments token collector architecture supports pluggable methods (e.g., Permit2 collectors). In the future,assetTransferMethodcould signal the client signing path when additional collectors are added — similar to howexactuses it to toggle betweeneip3009andpermit2.Related proposals: #839, #864, #946, #1247
Tests
Spec-only PR — no code changes. Verification and settlement logic validated against a reference implementation with passing E2E tests on Base Sepolia.
Checklist