Skip to content

Add authCapture scheme specification#1425

Merged
CarsonRoscoe merged 5 commits into
x402-foundation:mainfrom
BackTrackCo:feature/escrow-scheme-spec
May 13, 2026
Merged

Add authCapture scheme specification#1425
CarsonRoscoe merged 5 commits into
x402-foundation:mainfrom
BackTrackCo:feature/escrow-scheme-spec

Conversation

@A1igator
Copy link
Copy Markdown
Contributor

@A1igator A1igator commented Mar 3, 2026

At x402r.org, we've been working on refundable payments for x402. This PR introduces the authCapture scheme, built on Base's audited Commerce Payments Protocol. It supports two settlement paths (two-phase and single-shot), with client signing via either ERC-3009 or Permit2.

Refs: #834, #1011

Description

Adds the authCapture scheme specification as two files:

  • scheme_authCapture.md: Cross-VM scheme overview. Covers settlement paths (two-phase vs single-shot via autoCapture), lifecycle (authorize/capture/void/refund/reclaim), the captureAuthorizer role, and security considerations.
  • scheme_authCapture_evm.md: EVM implementation. Covers PaymentRequirements, PaymentPayload (ERC-3009 and Permit2 wire formats), verification logic, settlement logic, the on-chain PaymentInfo struct, fee system, universal CREATE2 contract addresses, and EIP-712 derivation tables.

Reuses audited Commerce Payments Protocol contracts (AuthCaptureEscrow + token collectors). Client signs a single ERC-3009 receiveWithAuthorization or Permit2 PermitTransferFrom, depending on the assetTransferMethod selected by the server.

Note on commit history: The first two commits in this PR introduced this scheme under the escrow and then commerce names. The latest commit renames the directory to authCapture, which more precisely describes the authorize-then-capture lifecycle and parallels the AuthCaptureEscrow contract name. The old commerce/ directory is removed in the same commit.

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.

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

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@cb-heimdall
Copy link
Copy Markdown

cb-heimdall commented Mar 3, 2026

🟡 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
Copy Markdown

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.

@ryanRfox
Copy link
Copy Markdown
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
Copy Markdown
Collaborator

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: https://github.com/coinbase/x402/pull/1474

@phdargen
Copy link
Copy Markdown
Collaborator

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
Copy Markdown
Collaborator

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
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm open to splitting it into 2 but variable input and settlement is basically identical meaning the schemes would be very similar (only difference being what function the facilitator calls, charge or authorize). That's why I thought it might make sense to have them together

Comment thread specs/schemes/escrow/scheme_escrow_evm.md Outdated
Comment thread specs/schemes/escrow/scheme_escrow_evm.md Outdated
Comment thread specs/schemes/escrow/scheme_escrow.md Outdated
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
Copy Markdown
Collaborator

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
Copy Markdown
Collaborator

@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

Copy link
Copy Markdown
Contributor Author

@A1igator A1igator Mar 10, 2026

Choose a reason for hiding this comment

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

So base commerce-payments is unopinionated on most of those questions and it would be implementation specific. You can check out our implementation at https://www.x402r.org/ .

I agree the naming might be a bit confusing so maybe "commerce" scheme would be better?

Regarding server failure and subjectiveness, correct. I will say one thing objective the middleware currently doesn't do is double check the payload matches a merchant set scheme but that could be added later.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yes I'd prefer "commerce" as scheme name aligning with the protocol it is build on

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Still not sure what the value add of the charge() mode is wrt exact. If funds are in the servers wallet, how do they flow back to the client? Can the operator claw back the funds? Or is it supposed to pay the refund out of its own pocket?

Seems to be strictly better and more straightforward to just keep the funds in the escrow until the challenge window is expired

Copy link
Copy Markdown
Contributor Author

@A1igator A1igator Mar 23, 2026

Choose a reason for hiding this comment

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

Charge (and refund as defined by base commerce-payments more specifically which is what charge is good for) is meant to be for scenarios where like a marketplace wants to guarantee certain amount of refunds as they only allow above certain reputation merchants for example. Another scenario is merchants themselves paying out of pocket or out of a bond they're supposed to put up.

Putting funds in the escrow for long periods of time is always strictly better for the client yes but it comes with the tradeoffs of merchant not getting their money fast even if they have a low refund rate that they could cover out of pocket.

Ditto on "commerce". Will rename next pass.

@phdargen
Copy link
Copy Markdown
Collaborator

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
Copy Markdown
Collaborator

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?

@A1igator
Copy link
Copy Markdown
Contributor Author

A1igator commented Mar 10, 2026

Hey all! Thanks for the thoughtful responses!

I will address and add more verification as per exact scheme upgrades. I will also generalize the escrow_scheme.md

Regarding the operator, base commerce-payments is agnostic to who the operator could be. It could be the facilitator (although personally not sure I'd recommend that as the facilitator is meant to do activities on behalf of a user not be a trusted entity itself), smart contracts like our operator factory produces at x402r (examples here: https://docs.x402r.org/contracts/examples), or a merchant platform like Shopify in the original commerce-payments announcement.

The paymentInfo and paymentInfo hash can work as payment id. I will investigate PAYMENT-RESPONSE to see if it makes sense to include! Thanks for the heads up!

Server capture again depends on operator implementation. They can in our setup at x402r.

- Generalize scheme_escrow.md to be network-agnostic
- Tighten verification: strict amount equality, tokenCollector
  recipient check, settlement simulation
- Add error codes section and assetTransferMethod note
- Simplify nonce derivation explanation
@A1igator
Copy link
Copy Markdown
Contributor Author

A1igator commented Mar 11, 2026

Hey all!I think I replied to or resolved all of the comments and pushed an update to the spec to generalize scheme_escrow.md, and tighten verification

I did check PAYMENT-RESPONSE and it does conceptually make sense to me. I checked the upto scheme and it seems to use SettlementResponse for that effect but SettlementResponse doesn't seem to be extendible for schemes currently? Only for extensions? So that seems to be blocked by another PR to fix that. I could add it to the spec but wanted to double check since implementation would be blocked.

Also regarding state, PaymentInfo and/or PaymentInfoHash have to be somehow tracked. How they are tracked somewhat depends on the operator implementation because the operator entity/contract could keep track. Otherwise the parties have to keep track of it themselves manually. Can add this clarification to the spec if makes sense there.

Thanks again for all the thoughtful responses!

A1igator referenced this pull request in BackTrackCo/docs Mar 19, 2026
## Summary

- **Escrow scheme spec rewritten** to align with [coinbase/x402 PR
#1425](https://github.com/coinbase/x402/pull/1425) and [issue
#1011](https://github.com/coinbase/x402/issues/1011) — adds settlement
methods (authorize/charge), nonce derivation, 11-step verification
logic, settlement logic, error codes, PaymentInfo struct with all
fields, and expiry ordering
- **All contract addresses updated** to unified CREATE3 addresses (same
on every chain)
- **Missing contracts added**: ArbiterRegistry, RefundRequestEvidence,
ReceiverRefundCollector, SignatureCondition/SignatureRefundRequest
factories, and all condition/combinator factories
- **Supported chains expanded** from 2 (Base + Base Sepolia) to 11
chains
- **PaymentInfo struct fixed** in architecture docs (was missing
`preApprovalExpiry`, `refundExpiry`, `salt`, had wrong field ordering)
- **Payment flow diagrams fixed** to route through operator (not escrow
directly)
- **Roadmap updated** with escrow scheme spec submission status and
multi-chain deployment

## Test plan

- [ ] Preview with `npx mint dev` and verify all pages render
- [ ] Verify all contract addresses match `@x402r/core` config
- [ ] Verify escrow scheme spec matches PR #1425 content
- [ ] Check all internal links work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
@phdargen
Copy link
Copy Markdown
Collaborator

phdargen commented Mar 23, 2026

Thanks a lot for the update @A1igator!

The changes look good but I think the spec still leaves too much out as implementation detail.
The fact that the base commerce protocol is unopinionated about the operator doesnt necessarily mean this applies to a x402 specific implementation too.

I think a client needs to be able to fully understand the entire payment lifecycle based on the initial 402 response. The authorization is well speced out, but everything after the initial request remains operator specific.

All the client knows is the operator address but how to infer from this alone how to interact with it after the initial request? If we leave all of this out of spec, a client needs to know out of band implementation specifics of each operator.

Once a client deposited in the escrow, how can they claim a refund? How do they know if their deposit has been charged? How can the server request a charge and when? On resource delivery or after refundExpirySeconds has passed?

I also think we should just define the facilitator as the operator. The facilitators role is the payment orchestration, so feels only natural (can I get your thoughts on this @fabrice-cheng?). This simplifies the spec and has it fully self-contained.

Then define new API interfaces for /capture /refund etc including who can call them when and under which circumstances. Do client/server need gas or is there a way for the facilitator to sponsor all operations?

@A1igator
Copy link
Copy Markdown
Contributor Author

A1igator commented Mar 23, 2026

Thanks for the suggestion @phdargen but I think providing information can be done without making facilitator the operator.

I would recommend a separate extension (like this one we've been building here: BackTrackCo/x402r-scheme#27 and will send over when ready) to get all the information you would like to pass through the 402 flow to let the user know how to interact with the operator.

Facilitator as the operator would a) Make the facilitator heavily trusted which goes against x402 principles as listed in the readme: "Trust minimizing: all payment schemes must not allow for the facilitator or resource server to move funds, other than in accordance with client intentions". b) Would break protocols like ours which let's you pick your third party arbiters which can even be chained together. c) Break smart contract operators (which we also have) which are important in trust minimizing the operator with things like escrow periods.

There might be a version of this scheme that bundles the extension above and scheme together but I feel like we're ballooning the proposal then and opinionating it towards the refund usecase. (it can also be used for sessions for example like the Agentokratia proposal which has nothing to do with refunds: https://github.com/coinbase/x402/issues/834)

Would also love to know @fabrice-cheng's thoughts!

Thanks again for the review!

@A1igator
Copy link
Copy Markdown
Contributor Author

A1igator commented Mar 23, 2026

I will say maybe we could have /capture /refund, etc... interfaces that simply enact a signature's request from the authorized party through to the contracts (same way settle works) if you'd like the facilitator to simply pay for gas for every operation. I need to spec this out properly though and figure out what signature scheme makes sense if wanted. I'm also not quite sure what the benefits are vs a paymaster but could be wrong.

Comment thread specs/schemes/escrow/scheme_escrow_evm.md Outdated
"extra": {
"name": "USDC",
"version": "2",
"escrowAddress": "0xEscrowAddress",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thoughts on defaulting x402 spec to a unique Escrow/TokenCollector (i.e: like Permit2)
or allowing this flexibility?

The payload is really specific to AuthCaptureEscrow smart contract

Copy link
Copy Markdown
Contributor Author

@A1igator A1igator Apr 9, 2026

Choose a reason for hiding this comment

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

Firstly, I think the flexibility should definitely be allowed because while base commerce-payments contracts are pretty good and well designed, there's lots of innovation still left possible at the escrow layer without breaking this scheme. For example:

  1. Adding partialVoid to allow for partial refunds in an escrow.
  2. Adding AAVE or other defi for capital efficiency for funds in the escrow.
  3. Gas cost improvements.

We already do 1 as it was asked for. TokenCollector's also appear to be tied to an AuthCaptureEscrow hence why they need to both be optional.

Now whether they should be optional and just default to the canonical ones if not set is a different question. Since our protocol already had to fork to do 1, we have to set it either way so this doesn't affect us but can add it if wanted.

"x402Version": 2,
"accepts": [
{
"scheme": "escrow",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

auth-capture vs escrow?

Copy link
Copy Markdown
Contributor Author

@A1igator A1igator Apr 9, 2026

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)

"escrowAddress": "0xEscrowAddress",
"operatorAddress": "0xOperatorAddress",
"tokenCollector": "0xCollectorAddress",
"settlementMethod": "authorize",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't this always be "authorize"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There's also "charge" as I explained above: #1425 (comment)

Implementation is 95% identical (just function call difference) so thought made sense to include here.

}
},
"payload": {
"authorization": {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need to pass this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Technically no but then some values like validAfter would be assumed. Depends how verbose/informational we want to be

@fabrice-cheng
Copy link
Copy Markdown
Contributor

Please add the contract Address in the spec as well! Thank you!

@A1igator
Copy link
Copy Markdown
Contributor Author

Hi @fabrice-cheng !

This is not a pushback but I just needed some clarification for implementing. A bit confused what's happening with salt. It used to be derived by client automatically but now it is manually selected by the merchant since it's required in the extra? Wanted to make sure this is an intentional change.

Since payer is always zero address in nonce calculation, the salt has to be fresh per request otherwise two different payers can have nonce collision. This means it should be automatically calculated and rotated by the server? I assume implementation wise that can be automatically filled via x402ResourceServer like name, version in exact?

Would love to know your thoughts!

@fabrice-cheng
Copy link
Copy Markdown
Contributor

@A1igator , I supposed it can be client-side generated, it's critical taht it's unique as you mentioned. I'm in favor of dropping it in the PaymentRequirements, and will have to be passed in via PaymentPayload then.

cc: @avidreder

@A1igator
Copy link
Copy Markdown
Contributor Author

Hi @fabrice-cheng

Sorry one more thing. Are we sure about the name "captureAuthorizer"?

This description: "Address authorized to capture/void/refund (server or facilitator)" is not really correct. It also has to do authorize. This means in the x402 flow, it practically either has to be the facilitator or a smart contract (since a smart contract operator can open up permissions to allow authorize and capture entity to be different). We have such smart contracts built out at x402r.org so it's fine for us but just wanted to flag the name/description being a bit misleading.

@fabrice-cheng
Copy link
Copy Markdown
Contributor

The Payment Authorization is the ERC3009 signature itself technically.

The captureAuthorizer identifies who has the permission to capture a payment or void a payment. Different profile

The Authorizer part of captureAuthorizer is to be consistent with the other x402 specs (x402BatchSettlement's naming of payerAuthorizer and receiverAuthorizer), which is super confusing because of payment authorization, but yeah..

@A1igator
Copy link
Copy Markdown
Contributor Author

The thing is the ERC3009 signature has to identify the operator/captureAuthorizer and it can only be settled (money sent to/through contracts) by said entity. Only the operator/captureAuthorizer is allowed to "authorize" (in addition to only being allowed to capture/void/refund)

image

More reading: https://github.com/base/commerce-payments/blob/main/docs/operations/Authorize.md

It has onlySender(paymentInfo.operator) gate. In x402 case, that means without smart contract operators, only the facilitator can be the captureAuthorizer since it also does the "authorize". As I mentioned above we have said contracts so it's fine but the name/description might confuse people thinking it only handles capture.

A1igator added a commit to BackTrackCo/x402r-scheme that referenced this pull request May 1, 2026
Restores the References section from scheme_commerce.md with the
corrected "Escrow Scheme Proposal" titles (per #36, superseded by the
commerce → authCapture rewrite). Convention matches the upstream
submission in x402-foundation/x402#1425.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…chema

Renames the scheme directory from commerce/ to authCapture/ and rewrites
the EVM spec to cover both ERC-3009 and Permit2 payment-collector paths.

Key spec additions:
- extra field schema (captureAuthorizer, captureDeadline, refundDeadline,
  feeRecipient, min/maxFeeBps, autoCapture, assetTransferMethod)
- Wire format + EIP-712 derivation tables for ERC-3009 and Permit2
- Spec -> on-chain PaymentInfo field mapping (preserves canonical Solidity
  names so EIP-712 typehash matches the AuthCaptureEscrow contract)
- Universal contract addresses block (CREATE2-deterministic)
- Verification step list aligned with facilitator implementation:
  preApprovalExpiry <= captureDeadline <= refundDeadline (>= allowed,
  matching contract _validatePayment)
@A1igator A1igator changed the title Add escrow scheme specification Add authCapture scheme specification May 1, 2026
@A1igator
Copy link
Copy Markdown
Contributor Author

A1igator commented May 1, 2026

Hey @fabrice-cheng !

Just pushed an update implementing all the changes you requested and canonical addresses pre-deployed on Base mainnet and Sepolia and others. Anyone can also deploy with the same address on any chain as I provided the salts in the spec! Updated PR title and description as well to match.

A few notes though:

  1. Kept the name captureAuthorizer for now but updated the description to "Address authorized to authorize/capture/void/refund/charge." to be more accurate. Let me know if you'd like a different name too knowing the description.
  2. I forgot to mention this because we don't use it but to be faithful to the original contracts, feeReciever can be set to address(0) to let the operator/captureAuthorizer specify any non-zero recipient at capture/charge time. Added that to the description but might want to consider making it optional.

We are happy with the spec as is though and would be happy to see it merged! We also almost have implementation of the scheme ready that we can push upstream as well: x402r-scheme.

Thanks for the feedback and review!

@levalleux-ludo
Copy link
Copy Markdown

Hi all — flagging this here as you've been following the escrow / buyer protection discussion.

We've just opened #2222, proposing scheme: "escrow" as a wire-format standard for non-custodial on-chain escrow in x402. It is intentionally implementation-agnostic: rather than standardising a specific contract, it standardises how buyer and seller communicate over HTTP, so that any of the escrow approaches already under discussion in this repository can publish conforming nextActions and become driveable by a single generic client.

The proposal includes an explicit relationship table covering the existing escrow-related issues and PRs (including this one), so it builds on rather than competes with the work already discussed here.

Your feedback would be very welcome — please take a look and weigh in directly on #2222.

@A1igator
Copy link
Copy Markdown
Contributor Author

A1igator commented May 7, 2026

Hey @levalleux-ludo !

Thanks for the suggestion and I don't mind this but I think we might be over-abstracting at this point and taking an unneeded UX hit and phishing/hack attack surface increase as nextActions seems pretty complicated for the average merchant. The Base commerce-payments escrows are not opinionated so any flow you'd like, including the Boson one, can be done as an operator contract. We can help provide it at x402r if you'd like as we're already doing for number of other partners or anyone can build it themselves.

x402 standard also seem to prefer locking schemes to canonical contracts as I already suggested making the escrow contract address flexible but that was shut down to match other schemes.

@fabrice-cheng
Copy link
Copy Markdown
Contributor

This looks great, thank you for putting this together.
@A1igator , one feedback, let's just reuse the existing AuthCaptureEscrow contract out there that is live.

We will be deploying those contracts on the other EVM chains to make sure the address are consistent

Defer to the live deployment from base/[email protected] instead
of redeploying via CREATE2. Drop the now-obsolete inline address tables
and salt-scheme paragraph; the Canonical Addresses annex now links to
the upstream release notes (and Uniswap deployments docs for Permit2),
matching scheme_exact_evm.md's third-party 'Canonical Permit2' annex
pattern.
@A1igator
Copy link
Copy Markdown
Contributor Author

A1igator commented May 8, 2026

Done @fabrice-cheng!

@levalleux-ludo
Copy link
Copy Markdown

Hey @levalleux-ludo !

Thanks for the suggestion and I don't mind this but I think we might be over-abstracting at this point and taking an unneeded UX hit and phishing/hack attack surface increase as nextActions seems pretty complicated for the average merchant. The Base commerce-payments escrows are not opinionated so any flow you'd like, including the Boson one, can be done as an operator contract. We can help provide it at x402r if you'd like as we're already doing for number of other partners or anyone can build it themselves.

x402 standard also seem to prefer locking schemes to canonical contracts as I already suggested making the escrow contract address flexible but that was shut down to match other schemes.

Thanks @A1igator — appreciate the careful read. A few short clarifications, point by point.

On nextActions. Worth flagging that in our reference implementation nextActions isn't merchant-authored. It's auto-derived server-side from the canonical state machine by the SDK (deriveNextActions(exchange, serverConfig)), much like the accepts[] block of a 402 response today. For a minimum-viable seller — commit → release, no disputes — the merchant doesn't write or reason about it. That said, your point about a clearer "minimum merchant" walkthrough is fair and we owe that in the docs.

On the phishing / attack-surface concern. Just to surface the relevant defenses: the buyer's authorization is EIP-712-signed against the escrow contract's verifyingContract and the specific action, and token authorizations (ERC-3009 / Permit / Permit2 / pre-approval) are amount- and destination-locked to escrowAddress. Unrecognized action IDs are skipped by clients (namespaced prefixes). The trust boundary for "is this the right escrow contract" sits in requirements, not in nextActions — which leads into your next point.

On expressing the Boson flow as an operator contract on BCP-Base. We've looked at this and there are a few places where it isn't yet obvious to us how the flow maps cleanly — for example, the operator role in AuthCaptureEscrow looks single-trust with buyer-only reclaim() as the fallback, whereas our flow needs unconditional on-chain progress paths for both parties in every non-terminal state; and BCP-Base's collector set is narrower than what escrow aims to cover (we also need Permit and pre-approval). These may well be misreadings on our side and we're not declaring them as blockers — just flagging that the mapping isn't trivial from where we sit.

On canonical contracts. Good signal that x402 prefers locking schemes to canonical addresses. We're open to that direction: an escrowId referencing a canonical registry would be a strict improvement over a free-form escrowAddress from our perspective, and we'd be happy to align there.

Happy to keep the broader scheme-design conversation on #2222 where it's already underway with the wider x402 group — easier to converge with the decision team in the loop. Thanks again.

@A1igator
Copy link
Copy Markdown
Contributor Author

A1igator commented May 11, 2026

Hey @levalleux-ludo !

I won't go too in depth here on most of your points regarding your proposal as you mentioned it belongs in the other thread. Still think #2222 is too complex whether that complexity falls on merchant or client or x402 maintainers.

Re expressing Boson flows though:

Firstly, there's token collectors for both permit2 and pre-approval: https://github.com/base/commerce-payments/blob/main/docs/TokenCollectors.md

Secondly, as I mentioned before the operator needs to be a smart contract to make things work not a single trusted party but "on-chain progress paths for both parties in every non-terminal state" is very much possible. We have some examples here: https://github.com/BackTrackCo/arbiter-examples. Kleros one for example is optimistic with funds automatically being released to the merchant after an escrow period, while the AI one is pessimistic with funds going back to the payer after an escrow period. Won't bog down this thread with too much specific arbiter style discussion but happy to discuss offline how to make Boson work.

Copy link
Copy Markdown
Contributor

@CarsonRoscoe CarsonRoscoe left a comment

Choose a reason for hiding this comment

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

Thank you @A1igator for the contribution 🙏

@CarsonRoscoe CarsonRoscoe merged commit 850e3ce into x402-foundation:main May 13, 2026
14 of 15 checks passed
@levalleux-ludo
Copy link
Copy Markdown

Hey @levalleux-ludo !

I won't go too in depth here on most of your points regarding your proposal as you mentioned it belongs in the other thread. Still think #2222 is too complex whether that complexity falls on merchant or client or x402 maintainers.

Re expressing Boson flows though:

Firstly, there's token collectors for both permit2 and pre-approval: https://github.com/base/commerce-payments/blob/main/docs/TokenCollectors.md

Secondly, as I mentioned before the operator needs to be a smart contract to make things work not a single trusted party but "on-chain progress paths for both parties in every non-terminal state" is very much possible. We have some examples here: https://github.com/BackTrackCo/arbiter-examples. Kleros one for example is optimistic with funds automatically being released to the merchant after an escrow period, while the AI one is pessimistic with funds going back to the payer after an escrow period. Won't bog down this thread with too much specific arbiter style discussion but happy to discuss offline how to make Boson work.

@A1igator — thanks for engaging.
On openness.
Starting there because it's the load-bearing point: we believe scheme: "escrow" has to be open, so that any escrow implementation can plug in under it. We noticed you originally argued for that same stance — what drove you to change your mind?
Different escrows exist for a reason. Boson made a specific set of design decisions and trade-offs, but other escrow types are warranted for other use cases — e.g. high-value items needing stronger verification, longer dispute windows, or different arbitration models. A scheme that bakes in one implementation forecloses those.

On the suggestion that Boson (or another escrow) could plug into authCapture.
Two problems:
(a) It isn't open — it hard-codes Coinbase's AuthCaptureEscrow contract as the settlement layer everything else has to sit on top of.
(b) The composition doesn't fit architecturally. Boson isn't an operator or an arbitrator — Boson includes its own escrow, with its own custody, state machine, and dispute logic. Nesting one escrow inside another duplicates those responsibilities rather than composing them. The same applies to cart.fun's TrustEngine, PayCrow, Arbitova, and others: they are escrows in their own right, not policy modules sitting on top of AuthCaptureEscrow.

On complexity. Fair point — we're looking into moving some of the multi-step-exchange surface out of the scheme itself, along the lines you suggested.

@A1igator
Copy link
Copy Markdown
Contributor Author

Hey @levalleux-ludo,

"e.g. high-value items needing stronger verification, longer dispute windows, or different arbitration models." are all possible with this scheme. Custody is the only thing this scheme "hard-codes" but that seems preferable to have a battle tested audited escrow. Any state machine and dispute logic is doable with this scheme.

This PR is already merged so don't want to discuss here and ping the maintainers for no reason but feel free to join our discord if you wanna learn more.

@avidreder
Copy link
Copy Markdown
Contributor

@A1igator did you have an in progress implementation of the Auth Capture SDK? I'm working on something for Go, so would love to see what you have so we can ensure implementations are consistent.

@A1igator
Copy link
Copy Markdown
Contributor Author

@avidreder yeah here's our TS implementation: https://github.com/BackTrackCo/x402r-scheme. Just doing examples before opening an upstream PR.

@levalleux-ludo
Copy link
Copy Markdown

Hey @levalleux-ludo,

"e.g. high-value items needing stronger verification, longer dispute windows, or different arbitration models." are all possible with this scheme. Custody is the only thing this scheme "hard-codes" but that seems preferable to have a battle tested audited escrow. Any state machine and dispute logic is doable with this scheme.

This PR is already merged so don't want to discuss here and ping the maintainers for no reason but feel free to join our discord if you wanna learn more.

Thanks @A1igator.
Two notes for the record before stepping back:

On custody as a feature, not a hard-code. An audited single contract is valuable at the implementation level — it's the foundation any conforming authCapture server inherits. Our point is upstream of that. A scheme-level commitment to a specific custody contract pins the wire format to one implementation's roadmap. The same audit reasoning that justifies AuthCaptureEscrow today applies to every future audited escrow that wants to be conformant under the same scheme — and they can be, if custody sits at the implementation layer rather than the scheme layer. That's the openness point; it doesn't take anything away from the contract you've built.

On "all possible via operator contracts". An operator solves the scheme-level openness question only at one of two costs. If the operator is an EOA, trust in the operator replaces trust in the seller — the escrow becomes redundant relative to direct payment. If the operator is a contract, the client must learn its full action surface per implementation — at which point you're back to per-implementation client knowledge, which is exactly the trap an open scheme is meant to prevent. Either way, the wire-format separation we're proposing is still load-bearing for any second conforming implementation.

Happy to leave PR #1425 as merged. For the open-scheme question we'll keep the substantive thread on Issue #2222 where the maintainers are already engaged.
Thanks for the Discord invite — we'll stay on the canonical GitHub surface for now.

@A1igator A1igator mentioned this pull request May 14, 2026
4 tasks
@A1igator
Copy link
Copy Markdown
Contributor Author

@avidreder here you go: #2308!

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

Labels

specs Spec changes or additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants