Conversation
Adds the `authCapture` scheme spec (cross-VM abstract + EVM implementation) and removes the `commerce` scheme spec it replaces. The EVM spec covers all 14 chains where the canonical commerce-payments v1 primitives are deployed, with a per-chain `assetTransferMethod` column noting which chains require Permit2 (BSC's Binance-Peg USDC and Tempo's pathUSD lack ERC-3009 support). Implementation in @x402r/evm tracks this spec one-to-one — see the companion PR for that. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Drop the trailing "contract's own access control" + assetTransferMethod caveats — both restate things implied by the rest of the section. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
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]>
|
Reviewed both spec files end-to-end. Two verifications, twelve findings, and an overall take. Verifications1. The spec already addresses the "captureAuthorizer also authorizes" concern. From the
And the bullet-list summary:
This captures the missing context cleanly. 2. PAYMENT-RESPONSE / lifecycle / capture-trigger — not addressed ❌ Nothing in either spec file talks about:
Cheapest fix is one explicit sentence: "Out-of-band lifecycle operations (capture / void / refund) are out of scope for this spec — see follow-up issue." Otherwise this question keeps getting re-derived by every reader. Findings (rough priority order)
Overall takeThe strong parts:
The actual concern: This spec nails the funds layer but punts on the lifecycle / orchestration layer in x402's stateless HTTP context. Scoping it that way is defensible — but downstream readers will keep asking the same questions until the spec explicitly says "lifecycle endpoints are out of scope, see follow-up X." Without that, every reviewer re-derives the same gap from scratch. A more aggressive option: spec out a minimal Naming friction is real but probably not blocking. Bottom line: Ship-ready once the verifications are addressed (#2 above is the substantive one), the lifecycle out-of-scope sentence added, and the verification-logic gaps closed (findings 1–3). Naming critiques are noise at this point. |
Two correctness gaps where the spec's verification list undercut what the facilitator actually checks: - Step 4 omitted minFeeBps from the required-extra fields list, even though the facilitator's isAuthCaptureExtra type guard requires it. - Step 6 listed only two deadline-ordering legs; the facilitator also enforces preApprovalExpiry <= captureDeadline (mirrors the contract's preApprovalExpiry <= authorizationExpiry leg of InvalidExpiries). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Pushed
Skipped the rest as either stylistic or expansions beyond the original commerce-spec / Coinbase-redesign baseline:
|
|
Verified Agree with the scoping calls on the rest — the strict One thing I'd still gently push for, even within "keep the spec tight": The one-sentence out-of-scope note for lifecycle ops. Not asking to spec the endpoints — just a line somewhere in Otherwise this looks ready. |
The contract's _validatePayment uses `preApprovalExp <= authorizationExp <= refundExp` (equal allowed). Spec was using strict `>` on the refund side, rejecting refundDeadline === captureDeadline configs that the contract itself accepts. Aligning to >= avoids the off-chain/on-chain divergence; the impl change ships alongside. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replaces the trailing prose paragraphs under EIP-3009 / Permit2 examples with explicit field-by-field tables. Closes the implementer gap on the EIP-712 domain construction (verifyingContract = asset for ERC-3009; canonical Permit2 contract for Permit2) and the validAfter = 0 convention, neither of which were spelled out before. Drops the standalone "salt is not in extra" callout — the placement is already shown in the JSON example and the PaymentInfo struct comments, and the singling-out was inconsistent with other PaymentInfo fields (payer / receiver / token / maxAmount / preApprovalExpiry) that are also not in extra without dedicated callouts. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Three reframings to make the spec read better cold (without prior commerce or PR-review context): - Drop the "addresses are not configurable per merchant" sentence in the Summary. The bullet list above already says the contracts have universal canonical addresses; restating it as a defensive aside only landed for readers comparing against an unstated alternative. - Reframe the address callout from "NOT in extra" to a positive "Universal contract addresses" header. The table is useful right next to the extra field list (saves a scroll to the appendix); the framing should be "here are the constants you need" rather than "here's what isn't in extra". - Rewrite the captureAuthorizer description to drop the `onlySender(paymentInfo.operator)` reference. The on-chain field name isn't introduced until the field-mapping table later in the doc, so the forward reference forced cold readers to skip ahead. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two changes: - Replace the 14-row deployed-chains table with a brief per-method token-requirement note and a pointer to base/commerce-payments for current deploy status. Chain coverage is deployment status, not protocol; pinning it here means the spec drifts every time a chain comes online or stables get added. The constraint isn't actually per-chain either — it's per-token (eip3009 needs receiveWithAuthorization-capable tokens, permit2 works with any ERC-20). Other x402 EVM specs (exact, upto) don't list chains. - Add a one-line note on assetTransferMethod that a server MAY include multiple accepts[] entries with different methods, so the client picks based on the tokens it holds. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The pointer paragraph claimed base/commerce-payments hosts the current chain-deployment list, but the upstream README only lists Base mainnet + Sepolia with the old prototype CREATE3 addresses (the ones our constants comment marks as superseded). The CREATE2 canonical deploys in our table are not documented there; pointing readers at it would hand them the wrong addresses. Other x402 EVM specs (exact, upto) just declare a canonical address + a one-line CREATE2 deploy intent. They do not link source repos for deployment status, do not list chains, and do not describe redeployment procedures. Following that convention. Also folds the per-method token requirement (eip3009 needs receiveWithAuthorization, permit2 works with any ERC-20) into the assetTransferMethod field description where it naturally belongs, instead of duplicating it in the appendix. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The table cell was three sentences. The eip3009 / permit2 capability description is lookup-able (both are well-known specs), and the multi-accepts hint is about accepts[] array usage more than about this specific field. Cell back to a one-liner; multi-accepts moved to a short note below the table. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two sentences in a cell is fine; lifting it out and back in was overkill. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Re-read the latest version. Three small bugs from the recent edits, plus one nuance worth surfacing. 1. Broken cross-reference (introduced by the appendix trim)Under the universal-addresses table:
The 2.
|
- Drop the "deployed-chain list and" phrase from the cross-reference to the Canonical Addresses appendix; the chain table was removed in 916bd37 so the link target no longer carries that. - Broaden the invalid_deadline_ordering error description to cover all three legs of step 6, not just the refundDeadline < captureDeadline leg. The facilitator surfaces this for any of the three failure modes. - Step 11 amount check phrasing was EIP-3009-leaning; spell out both authorization.value (EIP-3009) and permit2Authorization.permitted.amount (Permit2) so it's unambiguous. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Pushed
Skipped #4 — you flagged it as not-a-bug, and the spec stays consistent without it. Leaving the verification list at field-level checks rather than annotating the implicit transitivity through the nonce hash. |
A reader scanning the verification list might expect explicit checks for receiver, token (EIP-3009), deadlines, fee bounds, and feeRecipient. They aren't there because the payer-agnostic PaymentInfo hash already encodes them: any mismatch surfaces as nonce_mismatch at step 12. Calling that out so the absence isn't read as a gap. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Addressed #4 in |
|
Re-read end-to-end. All four items from the last round landed cleanly in Three small things worth flagging, then a step-back take. Things worth flagging1. The 2. Permit2 EIP-712 domain underspecified. The Permit2 field-derivation table just says "Canonical Permit2 contract; 3. Permit2 example That's ~6.4×10¹⁸ — the real nonce is Step-back takeThis spec is in genuinely good shape now. The verification list is tight, the field-derivation tables are a real readability win, the typed simulation reverts are unique value over Two structural choices that aren't bugs but worth knowing you're making: The lifecycle scope decision is implicit, not explicit. No sentence anywhere says "out-of-band capture / void / refund operations are out of scope for this scheme spec." A first-time reader walking through the spec will get to the end and ask "wait, but how does the captureAuthorizer actually trigger a capture?" — and there's no breadcrumb. You decided to leave it implicit; just be ready to answer that question on every review pass. I'd still add the sentence, but I'm done pushing on it. The "captureAuthorizer is also the authorizer" friction is real but well-handled. The naming is technically slightly inaccurate, but the bullet at the top of the EVM spec explains the on-chain gating clearly, so a careful reader gets it. The naming consistency with Bottom line: the only thing I'd still nudge on for survivability under wider review is the one-sentence lifecycle out-of-scope note. Everything else is polish. |
… nonce
- Document the feeRecipient = address(0) opt-out in the field description.
The Fee System appendix already says address(0) lets the caller pick a
non-zero recipient at capture/charge time, but the field description
marked feeRecipient as required without flagging this opt-in.
- Replace the Permit2 example nonce ("12345678901234567890", ~6.4×10¹⁸)
with a realistic 78-digit uint256 value. The real wire value is
uint256(payerAgnosticPaymentInfoHash); the previous placeholder
understated the field width and could mislead implementers testing
against the literal.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Pushed
|
|
Verified That's a wrap from me — combed the spec three times, no other nits. Approve. |
Two precisions in the role-attribution language:
- EVM spec settlement paragraph said "the facilitator submits it to
AuthCaptureEscrow.authorize()", which collapsed the two valid call
paths into a direct-call assumption. Reword to "The facilitator
calls ..., either directly or through a smart contract set as the
captureAuthorizer" — matches the captureAuthorizer definition
above and the on-chain msg.sender == operator gate.
- Settlement-paths table in the abstract spec used passive
"Refundable post-settlement" for autoCapture=true while the
autoCapture=false row spelled out the actor ("CaptureAuthorizer
can capture, void, refund"). Mirror the rows so a reader doesn't
infer the client can self-refund — the actor is the
captureAuthorizer in both paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ture + Permit2) (#40) > **Spec moved to a separate PR**: [#42](#42). This PR is now strictly the @x402r/evm package implementation. Read #42 first for the protocol surface; this PR is the code that implements it. Two-commit PR landing the full authCapture migration in the scheme package. ## Commit 1: rename `commerce` → `authCapture` (mechanical, identifier-only) - Source dir `packages/evm/src/commerce/` → `src/authCapture/` - Tests `packages/evm/test/unit/commerce/` → `test/unit/authCapture/` - Spec docs `specs/schemes/commerce/scheme_commerce*.md` → `specs/schemes/authCapture/scheme_authCapture*.md` - tsup entrypoints + package.json exports subpaths (`./commerce/*` → `./authCapture/*`) - Identifier renames: scheme constant, classes (`*EvmScheme`, `*ServerScheme`, `*FacilitatorScheme`), types (`*Payload`, `*Extra`), type guards, helpers (`computeCommerceNonce`, `registerCommerceEvmScheme`), error codes - `COMMERCE_PAYMENTS_*` constants and `commerce-payments` / `Base Commerce Payments` prose kept verbatim — they refer to the underlying base/commerce-payments protocol, not our scheme ## Commit 2: new wire format, salt on payload, autoCapture, Permit2 ### `extra` shape Drop: - `escrowAddress`, `operatorAddress`, `tokenCollector` — universal constants now - `preApprovalExpirySeconds` — client derives from top-level `maxTimeoutSeconds` - `authorizationExpirySeconds`, `refundExpirySeconds` — replaced by absolute deadlines - `settlementMethod` — replaced by `autoCapture` bool - `feeReceiver` — renamed `feeRecipient` Add: - `captureAuthorizer: address` (formerly `operatorAddress`) - `captureDeadline: uint48` / `refundDeadline: uint48` (absolute Unix seconds) - `feeRecipient: address` - `autoCapture?: bool` (default `false`) - `assetTransferMethod?: 'eip3009' | 'permit2'` (default `'eip3009'`) Spec field names live at the wire layer; the on-chain `PaymentInfo` struct keeps canonical Solidity field names (`operator`, `authorizationExpiry`, `refundExpiry`, `feeReceiver`) so the EIP-712 typehash matches `AuthCaptureEscrow.PAYMENT_INFO_TYPEHASH` byte-for-byte. ### `salt` placement Moved out of `extra` and onto `PaymentPayload`. Generated client-side at every `createPaymentPayload()` call, fresh per request. Facilitator reconstructs `PaymentInfo` from `payload.salt` + extra + payer + top-level requirements. Freshness is mandatory because the payer-agnostic nonce zeroes the payer field — two payers under identical extra+salt would collide. ### Constants module Renames: `COMMERCE_PAYMENTS_ESCROW` → `AUTH_CAPTURE_ESCROW_ADDRESS`, `COMMERCE_PAYMENTS_TOKEN_COLLECTOR` → `EIP3009_TOKEN_COLLECTOR_ADDRESS`. New: `PERMIT2_TOKEN_COLLECTOR_ADDRESS`, `PERMIT2_ADDRESS`, `PERMIT2_TRANSFER_FROM_TYPES`, `ESCROW_ABI` (was `OPERATOR_ABI` — the facilitator now calls `AuthCaptureEscrow` directly). `AuthCaptureFacilitatorScheme.getExtra()` now returns `undefined`: there's nothing facilitator-injected to advertise — addresses are constants, captureAuthorizer/feeRecipient/deadlines are merchant-set. ### Settle dispatch ``` extra.autoCapture === true → AuthCaptureEscrow.charge() (atomic) extra.autoCapture !== true → AuthCaptureEscrow.authorize() (two-phase) ``` ### Permit2 path New `Permit2Payload` variant in a discriminated union. Client signs Uniswap Permit2 `PermitTransferFrom` (no witness — merchant address is bound through the deterministic payer-agnostic nonce). Facilitator dispatches on `assetTransferMethod`; Permit2 collectorData is the ABI-encoded signature. ### Payer-zeroed nonce `computePayerAgnosticPaymentInfoHash()` zeroes the payer field before hashing. Same nonce regardless of payer; per-request freshness comes entirely from `salt`. Facilitator recomputes and asserts the wire nonce matches before settling. ## Tests `pnpm test` — 122 passing across 6 files. Coverage: - payer-agnosticism (different payers produce identical nonces) - salt freshness (consecutive `createPaymentPayload` calls produce distinct salts) - `autoCapture: true/false/undefined` settle routing - Permit2 payload construction + facilitator settle target - collector-mismatch (wrong `to` / `spender`) - payload-method-mismatch (Permit2 payload with `assetTransferMethod: 'eip3009'` and vice-versa) - deadline ordering enforcement ## Spec docs (in lockstep) `specs/schemes/authCapture/scheme_authCapture.md` and `scheme_authCapture_evm.md` rewritten to match the implementation. The EVM doc has full extra-field tables, both EIP-3009 and Permit2 payload shapes, the spec→on-chain field-name mapping table, payer-zeroed nonce derivation, full verification step list, and settle dispatch. ## Public API break Subpath imports change: `@x402r/evm/commerce/*` → `@x402r/evm/authCapture/*`. Wire format breaks: any caller building `extra` with old field names won't deserialize. Acceptable at `0.0.x`. Downstream consumers (`@x402r/core`, examples) need lockstep updates in follow-up PRs. ## Test plan - [x] `pnpm typecheck` clean - [x] `pnpm test` — 122/122 passing - [x] `pnpm build` — tsup ESM + CJS + DTS clean - [x] `pnpm format` clean - [x] `pnpm lint:check` clean - [ ] End-to-end Sepolia validation against canonical AuthCaptureEscrow deploy (separate PR once deploy addresses are pinned) - [ ] Update downstream consumers (`@x402r/core`, examples, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Summary
authCapturescheme spec — cross-VM abstract (scheme_authCapture.md) + EVM implementation (scheme_authCapture_evm.md).commercescheme spec (scheme_commerce.md+scheme_commerce_evm.md).Why this is a separate PR
The original PR #40 bundled spec + implementation; this PR carves the spec out so reviewers can read the protocol surface without wading through the @x402r/evm code diff. PR #40 is now strictly the implementation.
What's in the EVM spec
extrafield schema (captureAuthorizer,captureDeadline,refundDeadline,feeRecipient,min/maxFeeBps,autoCapture,assetTransferMethod)PaymentInfofield mapping (preserves canonical Solidity names so the EIP-712 typehash matches the contract byte-for-byte)AuthCaptureEscrow,ERC3009PaymentCollector,Permit2PaymentCollector— same address on every supported EVM chain, with salt labels so anyone can reproduceassetTransferMethodconstraint (BSC + Tempo are Permit2-only because their canonical stables lack ERC-3009)Test plan
scheme_authCapture.mdfor the abstract schemescheme_authCapture_evm.mdfor the EVM implementation contractbase/[email protected]source repo's deploy script🤖 Generated with Claude Code