Skip to content

Add escrow scheme specification#1425

Open
A1igator wants to merge 1 commit intocoinbase:mainfrom
BackTrackCo:feature/escrow-scheme-spec
Open

Add escrow scheme specification#1425
A1igator wants to merge 1 commit intocoinbase:mainfrom
BackTrackCo:feature/escrow-scheme-spec

Conversation

@A1igator
Copy link

@A1igator A1igator commented Mar 3, 2026

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: #834, #1011

Description

Adds the escrow scheme specification as two files:

  • scheme_escrow.md — Scheme overview: settlement methods (authorize/charge), lifecycle, relationship to exact, security considerations
  • scheme_escrow_evm.md — EVM implementation: PaymentRequirements, PaymentPayload, verification logic, settlement logic, PaymentInfo struct, fee system

Reuses audited Commerce Payments Protocol contracts (AuthCaptureEscrow, Operator, ERC3009PaymentCollector). Client signs a single ERC-3009 receiveWithAuthorization — same signature primitive as exact.

Notes for reviewers:

  1. Exact scheme spec/implementation mismatch: The exact EVM spec (step 5) says to "simulate token.transferWithAuthorization(...)" but the current TypeScript implementation in eip3009.ts doesn'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.

  2. assetTransferMethod as 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, assetTransferMethod could signal the client signing path when additional collectors are added — similar to how exact uses it to toggle between eip3009 and permit2.

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

  • I have formatted and linted my code
  • All new and existing tests pass
  • My commits are signed (required for merge)
  • I added a changelog fragment for user-facing changes (docs-only changes can skip) — spec-only, skipped

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>
@cb-heimdall
Copy link

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@vercel
Copy link

vercel bot commented Mar 3, 2026

@A1igator is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@fabrice-cheng
Copy link
Contributor

Thanks for the spec @A1igator - will be reviewing this shortly.

@ryanRfox
Copy link
Contributor

ryanRfox commented Mar 4, 2026

Good catch on the spec/implementation mismatch for simulation — that's exactly what #1377 tracks. The exact EVM spec requires simulation (step 5 in scheme_exact_evm.md) but no facilitator actually implements it, leading to gas loss on reverts (see #961, #418 for real-world examples).

There's a community contributor looking at a fix for exact (#1377). Once that lands, the escrow scheme's verification logic should probably align — a soft balance check (your step 10) catches some failures but misses things like consumed nonces, domain separator mismatches, and allowance issues that simulation would catch.

@phdargen
Copy link
Contributor

phdargen commented Mar 7, 2026

Good catch on the spec/implementation mismatch for simulation — that's exactly what #1377 tracks. The exact EVM spec requires simulation (step 5 in scheme_exact_evm.md) but no facilitator actually implements it, leading to gas loss on reverts (see #961, #418 for real-world examples).

There's a community contributor looking at a fix for exact (#1377). Once that lands, the escrow scheme's verification logic should probably align — a soft balance check (your step 10) catches some failures but misses things like consumed nonces, domain separator mismatches, and allowance issues that simulation would catch.

Yes verify should include tx simulation, we have a PR up to add it to the exact implementation here: #1474

@phdargen
Copy link
Contributor

phdargen commented Mar 7, 2026

Thanks for putting this together @A1igator!

As a general comment, scheme_escrow.md‎ should be network agnostic. Currently its too EVM specific and just reads as a lighter version of scheme_escrow_evm.md defying the point of splitting in 2 files. Instead scheme_escrow.md‎
should include abstract description, use cases and core properties without any network specifics

| `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"` |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

@phdargen phdargen Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@phdargen
Copy link
Contributor

phdargen commented Mar 7, 2026

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?

@phdargen
Copy link
Contributor

phdargen commented Mar 7, 2026

Exact works well for instant resource delivery like API responses. Its my understanding the primary use case of escrow would be delayed delivery (deferred settlement), for example e-commerce.

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 PAYMENT-RESPONSE, would this include sufficient info for the client to track the status of the payment or claim a refund?

Can the server request a capture? How can the server correlate captured/refunded payments with the original request?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

specs Spec changes or additions

Development

Successfully merging this pull request may close these issues.

5 participants