From 7719a7ba4f631f20908a08bd833e10205d5597d8 Mon Sep 17 00:00:00 2001 From: A1igator Date: Wed, 29 Apr 2026 17:45:21 -0700 Subject: [PATCH 01/18] spec: add authCapture, retire commerce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../schemes/authCapture/scheme_authCapture.md | 80 +++++ .../authCapture/scheme_authCapture_evm.md | 318 ++++++++++++++++++ specs/schemes/commerce/scheme_commerce.md | 86 ----- specs/schemes/commerce/scheme_commerce_evm.md | 238 ------------- 4 files changed, 398 insertions(+), 324 deletions(-) create mode 100644 specs/schemes/authCapture/scheme_authCapture.md create mode 100644 specs/schemes/authCapture/scheme_authCapture_evm.md delete mode 100644 specs/schemes/commerce/scheme_commerce.md delete mode 100644 specs/schemes/commerce/scheme_commerce_evm.md diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md new file mode 100644 index 0000000..3c47977 --- /dev/null +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -0,0 +1,80 @@ +# Scheme: `authCapture` + +## Summary + +`authCapture` 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 (two-phase) or sending them directly to the receiver with refund capability (single-shot). + +The **captureAuthorizer** is the entity authorized to authorize, capture, void, refund, or charge a payment. In a facilitator-submits flow, that's either the facilitator itself or any smart contract that ends up calling the underlying escrow. + +Unlike `exact`, which has no built-in mechanism for returning funds, `authCapture` 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 Paths + +The scheme supports two settlement paths, selected via `extra.autoCapture`: + +| `autoCapture` | Behavior | +| :---------------- | :--------------------------------------------------------------------------------------------------------------------------- | +| `false` (default) | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | +| `true` | Single-shot. Funds sent directly to receiver. Refundable post-settlement. | + +### Two-phase (`autoCapture: false`, 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 captureAuthorizer 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 captureAuthorizer can refund within the refund window. + +### Single-shot (`autoCapture: true`) + +``` +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 captureAuthorizer 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. +- Two-phase 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 and a fresh client-generated salt. +- Nonce is consumed on-chain at settlement, preventing double-spend. + +### Expiry Enforcement + +Two absolute-timestamp deadlines govern the payment lifecycle (network-specific implementations may add a derived pre-approval expiry from `maxTimeoutSeconds`): + +- **Capture deadline** (`captureDeadline`): Last moment to capture escrowed funds (two-phase); after this, the client can reclaim. +- **Refund deadline** (`refundDeadline`): Last moment to issue a refund on captured or charged payments. + +## Relationship to `exact` + +| Aspect | `exact` | `authCapture` | +| :--------- | :----------------- | :-------------------------------------------------------------------- | +| Settlement | Immediate transfer | Via escrow (two-phase) or direct with refund capability (single-shot) | +| 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_authCapture_evm.md` (EVM). diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md new file mode 100644 index 0000000..86ca22d --- /dev/null +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -0,0 +1,318 @@ +# Scheme: `authCapture` on `EVM` + +## Summary + +The `authCapture` scheme on EVM uses the [base/commerce-payments](https://github.com/base/commerce-payments) contract stack: + +- **AuthCaptureEscrow**: Singleton — locks funds, enforces expiries, distributes on capture/refund. Universal canonical address (CREATE2-deployed; same address on every supported chain). +- **Token Collectors**: Universal canonical addresses, one per `assetTransferMethod`: + - `EIP3009_TOKEN_COLLECTOR_ADDRESS` — collects funds via `receiveWithAuthorization` signatures (USDC, EURC, etc.) + - `PERMIT2_TOKEN_COLLECTOR_ADDRESS` — collects funds via Uniswap Permit2 `permitTransferFrom` (any ERC-20) +- **CaptureAuthorizer**: Address authorized to authorize, capture, void, refund, or charge a payment. Each of those methods on `AuthCaptureEscrow` is gated by `onlySender(paymentInfo.operator)`, so this address must be `msg.sender` of the "Authorize" call. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.) — the contract's own access control determines who can trigger it. Independent of `assetTransferMethod` — the constraint is on the escrow's sender check, not on the collector. + +The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it to `AuthCaptureEscrow.authorize()` (two-phase) or `AuthCaptureEscrow.charge()` (single-shot via `autoCapture: true`). + +The escrow + token-collector addresses are **not configurable per merchant** — they are universal constants. The wire format never carries them. + +## PaymentRequirements + +AuthCapture-accepting servers advertise with scheme `authCapture`: + +```json +{ + "x402Version": 2, + "accepts": [ + { + "scheme": "authCapture", + "network": "eip155:8453", + "amount": "1000000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0xReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { + "name": "USDC", + "version": "2", + "captureAuthorizer": "0xCaptureAuthorizerAddress", + "captureDeadline": 1740758554, + "refundDeadline": 1741276954, + "minFeeBps": 0, + "maxFeeBps": 1000, + "feeRecipient": "0xFeeRecipientAddress", + "autoCapture": false, + "assetTransferMethod": "eip3009" + } + } + ] +} +``` + +### `extra` Fields + +| Field | Required | Type | Description | +| :-------------------- | :------- | :----------------------- | :-------------------------------------------------------------------------------------------------------- | +| `name` | Yes | `string` | EIP-712 token-domain name (e.g., `"USDC"`). Used for ERC-3009 signing only. | +| `version` | Yes | `string` | EIP-712 token-domain version (e.g., `"2"`). | +| `captureAuthorizer` | Yes | `address` | Address authorized to authorize/capture/void/refund/charge. Committed on-chain as `PaymentInfo.operator`. | +| `captureDeadline` | Yes | `uint48` | Absolute Unix seconds — capture must occur before this. Encoded as `authorizationExpiry`. | +| `refundDeadline` | Yes | `uint48` | Absolute Unix seconds — refunds allowed until this. Encoded as `refundExpiry`. | +| `feeRecipient` | Yes | `address` | Fee recipient (committed on-chain as `PaymentInfo.feeReceiver`). | +| `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | +| `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | +| `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | +| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. | + +> **`salt` is NOT in `extra`.** It is generated client-side per signing call and rides on `PaymentPayload`. See "PaymentPayload" below. +> +> **Escrow + token-collector addresses are NOT in `extra`.** They are universal constants — same address on every supported EVM chain via deterministic CREATE2: +> +> | Constant | Address | +> | :------------------------------------ | :------------------------------------------- | +> | `AUTH_CAPTURE_ESCROW_ADDRESS` | `0xF8211868187974a7Fb9d99b8fFB171AD70665Dc6` | +> | `EIP3009_TOKEN_COLLECTOR_ADDRESS` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | +> | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | +> | `PERMIT2_ADDRESS` (Uniswap canonical) | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | +> +> See [Canonical Addresses](#canonical-addresses) for the deployed-chain list and the salt scheme. + +### Spec → on-chain field name mapping + +The wire-format extra uses spec-level field names. The on-chain `PaymentInfo` struct keeps canonical Solidity names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. + +| Wire (`extra`) | On-chain (`PaymentInfo`) | +| :----------------------------------- | :----------------------- | +| `captureAuthorizer` | `operator` | +| `captureDeadline` | `authorizationExpiry` | +| `refundDeadline` | `refundExpiry` | +| `feeRecipient` | `feeReceiver` | +| (derived: `now + maxTimeoutSeconds`) | `preApprovalExpiry` | + +## PaymentPayload + +The payload carries the signature and the client-generated `salt`. The facilitator reconstructs the full `PaymentInfo` from `extra` + `salt` + payer + top-level requirements (`payTo`, `asset`, `amount`). + +### EIP-3009 (default) + +```json +{ + "x402Version": 2, + "resource": { "url": "https://api.example.com/resource", "method": "GET" }, + "accepted": { "scheme": "authCapture", "...": "..." }, + "payload": { + "authorization": { + "from": "0xPayerAddress", + "to": "0xEIP3009TokenCollectorAddress", + "value": "1000000", + "validAfter": "0", + "validBefore": "1740675754", + "nonce": "0xf374...3480" + }, + "signature": "0x2d6a...571c", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + } +} +``` + +`authorization.to` is the universal `EIP3009_TOKEN_COLLECTOR_ADDRESS` constant. `validBefore` is `now + maxTimeoutSeconds`, also used as `preApprovalExpiry` when reconstructing PaymentInfo. + +### Permit2 + +```json +{ + "x402Version": 2, + "resource": { "url": "https://api.example.com/resource", "method": "GET" }, + "accepted": { "scheme": "authCapture", "...": "..." }, + "payload": { + "permit2Authorization": { + "from": "0xPayerAddress", + "permitted": { + "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "1000000" + }, + "spender": "0xPermit2TokenCollectorAddress", + "nonce": "12345678901234567890", + "deadline": "1740675754" + }, + "signature": "0x2d6a...571c", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + } +} +``` + +`spender` is the universal `PERMIT2_TOKEN_COLLECTOR_ADDRESS` constant. `nonce` is `uint256(payerAgnosticPaymentInfoHash)`. The `deadline` matches `now + maxTimeoutSeconds`. **No witness** — the merchant address is bound through the deterministic nonce. + +### Nonce Derivation (both methods) + +The signature nonce is the payer-agnostic `PaymentInfo` hash. Payer is zeroed; everything else is the values that will appear on-chain. + +``` +paymentInfoHash = keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, paymentInfoWithZeroPayer)) +nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash)) +``` + +Freshness is enforced by `salt`: each signing call generates a fresh `bytes32` salt, so two payers signing concurrently produce distinct nonces with no collision risk. + +## Verification Logic + +The facilitator performs these checks in order: + +1. **Type guard**: Verify payload matches one of `Eip3009Payload` or `Permit2Payload` (must include `signature` and `salt`). +2. **Scheme match**: `requirements.scheme === "authCapture"` and `payload.accepted.scheme === "authCapture"`. +3. **Network match**: `payload.accepted.network === requirements.network` and format is `eip155:`. +4. **Extra validation**: `requirements.extra` contains all required fields (`captureAuthorizer`, `captureDeadline`, `refundDeadline`, `feeRecipient`, `maxFeeBps`, `name`, `version`). +5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. +6. **Deadline ordering**: `refundDeadline > captureDeadline` and `captureDeadline > now + 6s`. +7. **Time window**: `payload.deadline / validBefore > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). +8. **Spender / collector match**: `payload.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `payload.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). +9. **Token match**: `payload.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). +10. **Signature verify**: Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `payer`. +11. **Amount**: Authorization value matches `requirements.amount`. +12. **Nonce match**: Reconstruct `PaymentInfo` from extra + payload.salt + payer + requirements; recompute payer-agnostic hash; assert it matches the wire nonce. +13. **Simulate** `AUTH_CAPTURE_ESCROW.authorize(...)` or `.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 + +1. **Re-verify** the payload (catches expired/invalid payloads before spending gas). +2. **Determine function**: `extra.autoCapture === true ? "charge" : "authorize"`. +3. **Resolve collector**: `EIP3009_TOKEN_COLLECTOR_ADDRESS` or `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (per `assetTransferMethod`). +4. **Encode `collectorData`**: raw ERC-3009 signature, or ABI-encoded Permit2 signature. +5. **Call escrow**: `AUTH_CAPTURE_ESCROW.(paymentInfo, amount, tokenCollector, collectorData)`. +6. **Wait for receipt**: 60s timeout. +7. **Return result**: tx hash, network, payer. + +## Error Codes + +The authCapture scheme uses the standard x402 error codes plus these scheme-specific codes: + +### Verification Errors + +| Error Code | Description | +| :---------------------------------- | :----------------------------------------------------------------------- | +| `invalid_payload_format` | Payload doesn't match `Eip3009Payload` or `Permit2Payload`. | +| `unsupported_scheme` | Scheme is not `authCapture`. | +| `network_mismatch` | Payload network doesn't match requirements. | +| `invalid_network` | Network format is not `eip155:`. | +| `invalid_authCapture_extra` | Extra is missing required fields. | +| `unsupported_asset_transfer_method` | `assetTransferMethod` is not `"eip3009"` or `"permit2"`. | +| `payload_method_mismatch` | Payload shape doesn't match `assetTransferMethod`. | +| `capture_deadline_expired` | `captureDeadline <= now + 6s`. | +| `invalid_deadline_ordering` | `refundDeadline <= captureDeadline`. | +| `authorization_expired` | EIP-3009 `validBefore` (or Permit2 `deadline`) `<= now + 6s`. | +| `authorization_not_yet_valid` | EIP-3009 `validAfter > now`. | +| `invalid_authCapture_signature` | Signature verification failed. | +| `amount_mismatch` | Authorization value doesn't match `requirements.amount`. | +| `token_collector_mismatch` | `to` / `spender` doesn't match the canonical collector for the method. | +| `token_mismatch` | Permit2 `permitted.token` doesn't match `requirements.asset`. | +| `nonce_mismatch` | Wire nonce doesn't match the recomputed payer-agnostic PaymentInfo hash. | +| `insufficient_balance` | Payer balance is less than required amount. | +| `simulation_failed` | Settlement simulation reverted with an unmapped error. | + +### Typed simulation reverts + +If the simulate call reverts with an `AuthCaptureEscrow` custom error declared in the call's ABI, the facilitator decodes it via `BaseError.walk()` + `ContractFunctionRevertedError` and surfaces a stable reason instead of the opaque `simulation_failed` fallback: + +| Custom error | `invalidReason` | +| :------------------------------ | :------------------------------------ | +| `AfterPreApprovalExpiry` | `authorization_expired` | +| `InvalidExpiries` | `invalid_deadline_ordering` | +| `ExceedsMaxAmount` | `amount_mismatch` | +| `PaymentAlreadyCollected` | `payment_already_collected` | +| `TokenCollectionFailed` | `token_collection_failed` | +| `InvalidCollectorForOperation` | `invalid_collector` | +| `InvalidSender` | `invalid_capture_authorizer` | +| `ZeroAmount` / `AmountOverflow` | `amount_mismatch` / `amount_overflow` | +| `FeeBpsOverflow` | `invalid_fee_bps` | +| `InvalidFeeBpsRange` | `invalid_fee_bps_range` | +| `FeeBpsOutOfRange` | `fee_bps_out_of_range` | +| `ZeroFeeReceiver` | `zero_fee_receiver` | +| `InvalidFeeReceiver` | `invalid_fee_receiver` | +| `AfterAuthorizationExpiry` | `capture_deadline_expired` | +| `InsufficientAuthorization` | `insufficient_authorization` | +| `ZeroAuthorization` | `zero_authorization` | + +### Settlement Errors + +| Error Code | Description | +| :--------------------- | :------------------------------------------------ | +| `verification_failed` | Re-verification before settlement failed. | +| `transaction_reverted` | On-chain transaction reverted after confirmation. | + +## Appendix + +### PaymentInfo Struct (canonical Solidity — wire-level field names map per the table above) + +```solidity +struct PaymentInfo { + address operator; // = extra.captureAuthorizer + address payer; // payload-derived + address receiver; // = requirements.payTo + address token; // = requirements.asset + uint120 maxAmount; // = requirements.amount + uint48 preApprovalExpiry; // = now + maxTimeoutSeconds (client-derived) + uint48 authorizationExpiry; // = extra.captureDeadline + uint48 refundExpiry; // = extra.refundDeadline + uint16 minFeeBps; + uint16 maxFeeBps; + address feeReceiver; // = extra.feeRecipient + uint256 salt; // = payload.salt (client-generated, fresh per request) +} +``` + +### Expiry Ordering + +The contract enforces: `preApprovalExpiry <= authorizationExpiry <= refundExpiry`. + +| Expiry | Wire field | Enforced at | Effect | +| :-------------------- | :---------------- | :------------------------- | :---------------------------------- | +| `preApprovalExpiry` | derived | `authorize()` / `charge()` | Blocks settlement after this time | +| `authorizationExpiry` | `captureDeadline` | `capture()` | Blocks capture; enables `reclaim()` | +| `refundExpiry` | `refundDeadline` | `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` (`extra.feeRecipient`) 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 + +### Canonical Addresses + +> **Requirement**: The escrow and token collectors are deployed at the same address across every supported EVM chain via deterministic CREATE2. Bytecode is byte-identical (locked compiler, optimizer, and dependency pins); anyone with the source can reproduce and verify the addresses, and any first-mover deployer who broadcasts the canonical bytecode at the canonical salt lands at the same address. + +**Source**: [base/commerce-payments@v1.0.0](https://github.com/base/commerce-payments/releases/tag/v1.0.0). + +**Salt scheme**: `bytes20(0) || 0x00 || bytes11(keccak256(label))`. The leading 21 bytes are constant; the label is the per-contract namespace below. + +| Constant | Salt label | Canonical Address | +| :------------------------------------ | :------------------------------------------------ | :------------------------------------------- | +| `AUTH_CAPTURE_ESCROW_ADDRESS` | `commerce-payments::v1::AuthCaptureEscrow` | `0xF8211868187974a7Fb9d99b8fFB171AD70665Dc6` | +| `EIP3009_TOKEN_COLLECTOR_ADDRESS` | `commerce-payments::v1::ERC3009PaymentCollector` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | +| `PERMIT2_TOKEN_COLLECTOR_ADDRESS` | `commerce-payments::v1::Permit2PaymentCollector` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | +| `PERMIT2_ADDRESS` (Uniswap canonical) | (Uniswap canonical, not CREATE2'd by this scheme) | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | + +**Deployed chains**: + +| Network | Chain ID | `assetTransferMethod` | +| :---------------- | :------- | :--------------------------------------------- | +| Ethereum | 1 | `eip3009` (Circle USDC) or `permit2` | +| Base | 8453 | `eip3009` (Circle USDC) or `permit2` | +| Optimism | 10 | `eip3009` (Circle USDC) or `permit2` | +| Arbitrum One | 42161 | `eip3009` (Circle USDC) or `permit2` | +| Polygon | 137 | `eip3009` (Circle USDC) or `permit2` | +| Celo | 42220 | `eip3009` (Circle USDC) or `permit2` | +| Avalanche C-Chain | 43114 | `eip3009` (Circle USDC) or `permit2` | +| Linea | 59144 | `eip3009` (Circle USDC) or `permit2` | +| Monad | 143 | `eip3009` (Circle USDC) or `permit2` | +| BNB Smart Chain | 56 | `permit2` only (Binance-Peg USDC, no ERC-3009) | +| Tempo | 4217 | `permit2` only (pathUSD TIP-20, no ERC-3009) | +| Ethereum Sepolia | 11155111 | `eip3009` (Circle USDC) or `permit2` | +| Base Sepolia | 84532 | `eip3009` (Circle USDC) or `permit2` | +| Arbitrum Sepolia | 421614 | `eip3009` (Circle USDC) or `permit2` | + +Facilitators that wish to add a chain not in this table SHOULD reproduce the canonical bytecode using the source repo's pinned compiler / optimizer settings and broadcast at the salt labels above; the addresses will match the table by construction. diff --git a/specs/schemes/commerce/scheme_commerce.md b/specs/schemes/commerce/scheme_commerce.md deleted file mode 100644 index 12e5f67..0000000 --- a/specs/schemes/commerce/scheme_commerce.md +++ /dev/null @@ -1,86 +0,0 @@ -# Scheme: `commerce` - -## Summary - -`commerce` 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, `commerce` 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` | `commerce` | -| :--------- | :----------------- | :--------------------------------------------------------------- | -| 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_commerce_evm.md` (EVM). - -### References - -- [Commerce Scheme Proposal — Agentokratia (Issue #834)](https://github.com/coinbase/x402/issues/834) -- [Commerce Scheme Proposal — x402r (Issue #1011)](https://github.com/coinbase/x402/issues/1011) diff --git a/specs/schemes/commerce/scheme_commerce_evm.md b/specs/schemes/commerce/scheme_commerce_evm.md deleted file mode 100644 index a1d39ad..0000000 --- a/specs/schemes/commerce/scheme_commerce_evm.md +++ /dev/null @@ -1,238 +0,0 @@ -# Scheme: `commerce` on `EVM` - -## Summary - -The `commerce` 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 commerce 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 - -Commerce-accepting servers advertise with scheme `commerce`: - -```json -{ - "x402Version": 2, - "accepts": [ - { - "scheme": "commerce", - "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", - "preApprovalExpirySeconds": 3600, - "authorizationExpirySeconds": 86400, - "refundExpirySeconds": 604800 - } - } - ] -} -``` - -### `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` | Seconds until pre-approval / ERC-3009 sig expires | -| `authorizationExpirySeconds` | No | `uint48` | Seconds until capture deadline for escrowed funds | -| `refundExpirySeconds` | No | `uint48` | Seconds until refund request deadline | - -## PaymentPayload - -```json -{ - "x402Version": 2, - "resource": { - "url": "https://api.example.com/resource", - "method": "GET" - }, - "accepted": { - "scheme": "commerce", - "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": "1740675754", - "nonce": "0xf374...3480" - }, - "signature": "0x2d6a...571c", - "paymentInfo": { - "operator": "0xOperatorAddress", - "receiver": "0xReceiverAddress", - "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "maxAmount": "1000000", - "preApprovalExpiry": 1740675754, - "authorizationExpiry": 1740758554, - "refundExpiry": 1741276954, - "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 === "commerce"` and `payload.accepted.scheme === "commerce"` -3. **Network match**: Verify `payload.accepted.network === requirements.network` and format is `eip155:` -4. **Extra validation**: Verify `requirements.extra` contains required commerce 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.(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 commerce 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 `commerce` | -| `network_mismatch` | Payload network does not match requirements | -| `invalid_network` | Network format is not `eip155:` | -| `invalid_commerce_extra` | Missing required extra fields (`escrowAddress`, `operatorAddress`, `tokenCollector`) | -| `authorization_expired` | `validBefore <= now + 6s` | -| `authorization_not_yet_valid` | `validAfter > now` | -| `invalid_commerce_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 From 5d3fdae80dc4ea740f3844d9ab7d8f4a037293e2 Mon Sep 17 00:00:00 2001 From: A1igator Date: Wed, 29 Apr 2026 17:49:38 -0700 Subject: [PATCH 02/18] spec: trim CaptureAuthorizer note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 86ca22d..23a9b4a 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -8,7 +8,7 @@ The `authCapture` scheme on EVM uses the [base/commerce-payments](https://github - **Token Collectors**: Universal canonical addresses, one per `assetTransferMethod`: - `EIP3009_TOKEN_COLLECTOR_ADDRESS` — collects funds via `receiveWithAuthorization` signatures (USDC, EURC, etc.) - `PERMIT2_TOKEN_COLLECTOR_ADDRESS` — collects funds via Uniswap Permit2 `permitTransferFrom` (any ERC-20) -- **CaptureAuthorizer**: Address authorized to authorize, capture, void, refund, or charge a payment. Each of those methods on `AuthCaptureEscrow` is gated by `onlySender(paymentInfo.operator)`, so this address must be `msg.sender` of the "Authorize" call. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.) — the contract's own access control determines who can trigger it. Independent of `assetTransferMethod` — the constraint is on the escrow's sender check, not on the collector. +- **CaptureAuthorizer**: Address authorized to authorize, capture, void, refund, or charge a payment. Each of those methods on `AuthCaptureEscrow` is gated by `onlySender(paymentInfo.operator)`, so this address must be `msg.sender` of the "Authorize" call. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.). The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it to `AuthCaptureEscrow.authorize()` (two-phase) or `AuthCaptureEscrow.charge()` (single-shot via `autoCapture: true`). From ef417adc174e22c6f146d4a966afb4fe9a6eb6cf Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 19:18:03 -0700 Subject: [PATCH 03/18] authCapture: carry forward upstream issue references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- specs/schemes/authCapture/scheme_authCapture.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index 3c47977..b4b86e3 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -78,3 +78,8 @@ Two absolute-timestamp deadlines govern the payment lifecycle (network-specific ## Appendix Network-specific implementation details (contracts, signature formats, verification logic) are in per-network documents: `scheme_authCapture_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) From daf968f27ba9d5c912ea9cdb141232979fe98aa2 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 20:06:51 -0700 Subject: [PATCH 04/18] authCapture: align verification step list with facilitator impl 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) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 23a9b4a..53a307c 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -158,9 +158,9 @@ The facilitator performs these checks in order: 1. **Type guard**: Verify payload matches one of `Eip3009Payload` or `Permit2Payload` (must include `signature` and `salt`). 2. **Scheme match**: `requirements.scheme === "authCapture"` and `payload.accepted.scheme === "authCapture"`. 3. **Network match**: `payload.accepted.network === requirements.network` and format is `eip155:`. -4. **Extra validation**: `requirements.extra` contains all required fields (`captureAuthorizer`, `captureDeadline`, `refundDeadline`, `feeRecipient`, `maxFeeBps`, `name`, `version`). +4. **Extra validation**: `requirements.extra` contains all required fields (`captureAuthorizer`, `captureDeadline`, `refundDeadline`, `feeRecipient`, `minFeeBps`, `maxFeeBps`, `name`, `version`). 5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. -6. **Deadline ordering**: `refundDeadline > captureDeadline` and `captureDeadline > now + 6s`. +6. **Deadline ordering**: `refundDeadline > captureDeadline`, `captureDeadline > now + 6s`, and `payload.validBefore` (EIP-3009) / `payload.deadline` (Permit2) `<= captureDeadline`. 7. **Time window**: `payload.deadline / validBefore > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). 8. **Spender / collector match**: `payload.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `payload.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). 9. **Token match**: `payload.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). From 635e6a97d86f3a9fdab8135bbc5bace980d4e37e Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 20:23:07 -0700 Subject: [PATCH 05/18] authCapture: relax refundDeadline ordering to >= for contract parity 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) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 53a307c..033e070 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -160,7 +160,7 @@ The facilitator performs these checks in order: 3. **Network match**: `payload.accepted.network === requirements.network` and format is `eip155:`. 4. **Extra validation**: `requirements.extra` contains all required fields (`captureAuthorizer`, `captureDeadline`, `refundDeadline`, `feeRecipient`, `minFeeBps`, `maxFeeBps`, `name`, `version`). 5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. -6. **Deadline ordering**: `refundDeadline > captureDeadline`, `captureDeadline > now + 6s`, and `payload.validBefore` (EIP-3009) / `payload.deadline` (Permit2) `<= captureDeadline`. +6. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and `payload.validBefore` (EIP-3009) / `payload.deadline` (Permit2) `<= captureDeadline`. Matches the contract's `preApprovalExp <= authorizationExp <= refundExp` invariant. 7. **Time window**: `payload.deadline / validBefore > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). 8. **Spender / collector match**: `payload.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `payload.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). 9. **Token match**: `payload.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). @@ -199,7 +199,7 @@ The authCapture scheme uses the standard x402 error codes plus these scheme-spec | `unsupported_asset_transfer_method` | `assetTransferMethod` is not `"eip3009"` or `"permit2"`. | | `payload_method_mismatch` | Payload shape doesn't match `assetTransferMethod`. | | `capture_deadline_expired` | `captureDeadline <= now + 6s`. | -| `invalid_deadline_ordering` | `refundDeadline <= captureDeadline`. | +| `invalid_deadline_ordering` | `refundDeadline < captureDeadline`. | | `authorization_expired` | EIP-3009 `validBefore` (or Permit2 `deadline`) `<= now + 6s`. | | `authorization_not_yet_valid` | EIP-3009 `validAfter > now`. | | `invalid_authCapture_signature` | Signature verification failed. | From fb64013cec1eead8392e7992aba93597e86db52f Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 20:27:35 -0700 Subject: [PATCH 06/18] authCapture: drop redundant contract-parity note in step 6 Co-Authored-By: Claude Opus 4.7 (1M context) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 033e070..2e75437 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -160,7 +160,7 @@ The facilitator performs these checks in order: 3. **Network match**: `payload.accepted.network === requirements.network` and format is `eip155:`. 4. **Extra validation**: `requirements.extra` contains all required fields (`captureAuthorizer`, `captureDeadline`, `refundDeadline`, `feeRecipient`, `minFeeBps`, `maxFeeBps`, `name`, `version`). 5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. -6. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and `payload.validBefore` (EIP-3009) / `payload.deadline` (Permit2) `<= captureDeadline`. Matches the contract's `preApprovalExp <= authorizationExp <= refundExp` invariant. +6. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and `payload.validBefore` (EIP-3009) / `payload.deadline` (Permit2) `<= captureDeadline`. 7. **Time window**: `payload.deadline / validBefore > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). 8. **Spender / collector match**: `payload.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `payload.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). 9. **Token match**: `payload.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). From 401f7955ae891efa125b59c53826361a3c664946 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 20:42:53 -0700 Subject: [PATCH 07/18] authCapture: add field-derivation tables for both payload methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../authCapture/scheme_authCapture_evm.md | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 2e75437..4fc6985 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -61,8 +61,6 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | | `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. | -> **`salt` is NOT in `extra`.** It is generated client-side per signing call and rides on `PaymentPayload`. See "PaymentPayload" below. -> > **Escrow + token-collector addresses are NOT in `extra`.** They are universal constants — same address on every supported EVM chain via deterministic CREATE2: > > | Constant | Address | @@ -112,7 +110,18 @@ The payload carries the signature and the client-generated `salt`. The facilitat } ``` -`authorization.to` is the universal `EIP3009_TOKEN_COLLECTOR_ADDRESS` constant. `validBefore` is `now + maxTimeoutSeconds`, also used as `preApprovalExpiry` when reconstructing PaymentInfo. +**Field derivation (EIP-3009):** + +| Payload field | Derived from | +| :-------------------------- | :---------------------------------------------------------------------------------------------------- | +| `authorization.from` | Client's own address | +| `authorization.to` | `EIP3009_TOKEN_COLLECTOR_ADDRESS` (universal constant) | +| `authorization.value` | `requirements.amount` | +| `authorization.validAfter` | `0` (the token collector hardcodes the lower bound) | +| `authorization.validBefore` | `now + requirements.maxTimeoutSeconds` (also used as `preApprovalExpiry` when reconstructing `PaymentInfo`) | +| `authorization.nonce` | Payer-agnostic `PaymentInfo` hash — see [Nonce Derivation](#nonce-derivation-both-methods) | +| `salt` | Fresh `bytes32` generated client-side per signing call | +| EIP-712 domain | `{ name, version }` from `extra`; `chainId` from `network`; `verifyingContract = requirements.asset` | ### Permit2 @@ -138,7 +147,20 @@ The payload carries the signature and the client-generated `salt`. The facilitat } ``` -`spender` is the universal `PERMIT2_TOKEN_COLLECTOR_ADDRESS` constant. `nonce` is `uint256(payerAgnosticPaymentInfoHash)`. The `deadline` matches `now + maxTimeoutSeconds`. **No witness** — the merchant address is bound through the deterministic nonce. +**Field derivation (Permit2):** + +| Payload field | Derived from | +| :--------------------------------------- | :---------------------------------------------------------------------------------------------------- | +| `permit2Authorization.from` | Client's own address | +| `permit2Authorization.permitted.token` | `requirements.asset` | +| `permit2Authorization.permitted.amount` | `requirements.amount` | +| `permit2Authorization.spender` | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (universal constant) | +| `permit2Authorization.nonce` | `uint256(payerAgnosticPaymentInfoHash)` — see [Nonce Derivation](#nonce-derivation-both-methods) | +| `permit2Authorization.deadline` | `now + requirements.maxTimeoutSeconds` (also used as `preApprovalExpiry` when reconstructing `PaymentInfo`) | +| `salt` | Fresh `bytes32` generated client-side per signing call | +| EIP-712 domain | Canonical Permit2 contract; `chainId` from `network` | + +**No witness** — the merchant address is bound through the deterministic nonce, not a separate witness struct. ### Nonce Derivation (both methods) From 18c9e60d33e16e3db46e1cc70b6998eef9396588 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 20:48:59 -0700 Subject: [PATCH 08/18] authCapture: trim defensive framing for fresh readers 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) --- .../authCapture/scheme_authCapture_evm.md | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 4fc6985..78a0fcf 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -8,12 +8,10 @@ The `authCapture` scheme on EVM uses the [base/commerce-payments](https://github - **Token Collectors**: Universal canonical addresses, one per `assetTransferMethod`: - `EIP3009_TOKEN_COLLECTOR_ADDRESS` — collects funds via `receiveWithAuthorization` signatures (USDC, EURC, etc.) - `PERMIT2_TOKEN_COLLECTOR_ADDRESS` — collects funds via Uniswap Permit2 `permitTransferFrom` (any ERC-20) -- **CaptureAuthorizer**: Address authorized to authorize, capture, void, refund, or charge a payment. Each of those methods on `AuthCaptureEscrow` is gated by `onlySender(paymentInfo.operator)`, so this address must be `msg.sender` of the "Authorize" call. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.). +- **`captureAuthorizer`**: Address authorized to authorize, capture, void, refund, or charge a payment. The escrow contract gates those operations on `msg.sender` matching this address, so whichever party calls the escrow on the merchant's behalf must come from this address. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.). The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it to `AuthCaptureEscrow.authorize()` (two-phase) or `AuthCaptureEscrow.charge()` (single-shot via `autoCapture: true`). -The escrow + token-collector addresses are **not configurable per merchant** — they are universal constants. The wire format never carries them. - ## PaymentRequirements AuthCapture-accepting servers advertise with scheme `authCapture`: @@ -61,16 +59,16 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | | `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. | -> **Escrow + token-collector addresses are NOT in `extra`.** They are universal constants — same address on every supported EVM chain via deterministic CREATE2: -> -> | Constant | Address | -> | :------------------------------------ | :------------------------------------------- | -> | `AUTH_CAPTURE_ESCROW_ADDRESS` | `0xF8211868187974a7Fb9d99b8fFB171AD70665Dc6` | -> | `EIP3009_TOKEN_COLLECTOR_ADDRESS` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | -> | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | -> | `PERMIT2_ADDRESS` (Uniswap canonical) | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | -> -> See [Canonical Addresses](#canonical-addresses) for the deployed-chain list and the salt scheme. +**Universal contract addresses** (same on every supported EVM chain via deterministic CREATE2): + +| Constant | Address | +| :------------------------------------ | :------------------------------------------- | +| `AUTH_CAPTURE_ESCROW_ADDRESS` | `0xF8211868187974a7Fb9d99b8fFB171AD70665Dc6` | +| `EIP3009_TOKEN_COLLECTOR_ADDRESS` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | +| `PERMIT2_TOKEN_COLLECTOR_ADDRESS` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | +| `PERMIT2_ADDRESS` (Uniswap canonical) | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | + +See [Canonical Addresses](#canonical-addresses) for the deployed-chain list and the salt scheme. ### Spec → on-chain field name mapping From eac15ad37a26b01b0a99795dc34c1608bfce6af2 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 20:50:32 -0700 Subject: [PATCH 09/18] authCapture: drop redundant clause in captureAuthorizer summary Co-Authored-By: Claude Opus 4.7 (1M context) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 78a0fcf..ffe3a68 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -8,7 +8,7 @@ The `authCapture` scheme on EVM uses the [base/commerce-payments](https://github - **Token Collectors**: Universal canonical addresses, one per `assetTransferMethod`: - `EIP3009_TOKEN_COLLECTOR_ADDRESS` — collects funds via `receiveWithAuthorization` signatures (USDC, EURC, etc.) - `PERMIT2_TOKEN_COLLECTOR_ADDRESS` — collects funds via Uniswap Permit2 `permitTransferFrom` (any ERC-20) -- **`captureAuthorizer`**: Address authorized to authorize, capture, void, refund, or charge a payment. The escrow contract gates those operations on `msg.sender` matching this address, so whichever party calls the escrow on the merchant's behalf must come from this address. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.). +- **`captureAuthorizer`**: Address authorized to authorize, capture, void, refund, or charge a payment. The escrow contract gates those operations on `msg.sender` matching this address. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.). The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it to `AuthCaptureEscrow.authorize()` (two-phase) or `AuthCaptureEscrow.charge()` (single-shot via `autoCapture: true`). From 6167fb5726604abcbac4cb253b8633a1c2eae4b7 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 20:57:21 -0700 Subject: [PATCH 10/18] authCapture: drop deployed-chains table, abstract method requirements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../authCapture/scheme_authCapture_evm.md | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index ffe3a68..48f2216 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -57,7 +57,7 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | | `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | | `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | -| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. | +| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. A server MAY include multiple `accepts[]` entries with different `assetTransferMethod` values to let the client choose based on the tokens it holds. | **Universal contract addresses** (same on every supported EVM chain via deterministic CREATE2): @@ -316,23 +316,9 @@ Fees are enforced on-chain by the escrow contract: | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` | `commerce-payments::v1::Permit2PaymentCollector` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | | `PERMIT2_ADDRESS` (Uniswap canonical) | (Uniswap canonical, not CREATE2'd by this scheme) | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | -**Deployed chains**: - -| Network | Chain ID | `assetTransferMethod` | -| :---------------- | :------- | :--------------------------------------------- | -| Ethereum | 1 | `eip3009` (Circle USDC) or `permit2` | -| Base | 8453 | `eip3009` (Circle USDC) or `permit2` | -| Optimism | 10 | `eip3009` (Circle USDC) or `permit2` | -| Arbitrum One | 42161 | `eip3009` (Circle USDC) or `permit2` | -| Polygon | 137 | `eip3009` (Circle USDC) or `permit2` | -| Celo | 42220 | `eip3009` (Circle USDC) or `permit2` | -| Avalanche C-Chain | 43114 | `eip3009` (Circle USDC) or `permit2` | -| Linea | 59144 | `eip3009` (Circle USDC) or `permit2` | -| Monad | 143 | `eip3009` (Circle USDC) or `permit2` | -| BNB Smart Chain | 56 | `permit2` only (Binance-Peg USDC, no ERC-3009) | -| Tempo | 4217 | `permit2` only (pathUSD TIP-20, no ERC-3009) | -| Ethereum Sepolia | 11155111 | `eip3009` (Circle USDC) or `permit2` | -| Base Sepolia | 84532 | `eip3009` (Circle USDC) or `permit2` | -| Arbitrum Sepolia | 421614 | `eip3009` (Circle USDC) or `permit2` | - -Facilitators that wish to add a chain not in this table SHOULD reproduce the canonical bytecode using the source repo's pinned compiler / optimizer settings and broadcast at the salt labels above; the addresses will match the table by construction. +**Method requirements**: + +- `eip3009`: requires the token to implement `receiveWithAuthorization` (ERC-3009). Examples: Circle USDC, EURC. +- `permit2`: works with any ERC-20. + +For the current list of chains where the canonical contracts are live and which stablecoins each chain's deploy supports, see [base/commerce-payments](https://github.com/base/commerce-payments). Facilitators on chains without a live deploy SHOULD reproduce the canonical bytecode using the source repo's pinned compiler / optimizer settings and broadcast at the salt labels above; the resulting addresses match the constants in the table by construction. From 916bd37ea4d2341dbd2168704258222743e1f205 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 21:00:10 -0700 Subject: [PATCH 11/18] authCapture: trim canonical-addresses appendix to upstream convention 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) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 48f2216..37c332a 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -57,7 +57,7 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | | `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | | `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | -| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. A server MAY include multiple `accepts[]` entries with different `assetTransferMethod` values to let the client choose based on the tokens it holds. | +| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. `eip3009` requires the token to implement `receiveWithAuthorization` (e.g., Circle USDC, EURC); `permit2` works with any ERC-20. A server MAY include multiple `accepts[]` entries with different `assetTransferMethod` values to let the client choose based on the tokens it holds. | **Universal contract addresses** (same on every supported EVM chain via deterministic CREATE2): @@ -315,10 +315,3 @@ Fees are enforced on-chain by the escrow contract: | `EIP3009_TOKEN_COLLECTOR_ADDRESS` | `commerce-payments::v1::ERC3009PaymentCollector` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` | `commerce-payments::v1::Permit2PaymentCollector` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | | `PERMIT2_ADDRESS` (Uniswap canonical) | (Uniswap canonical, not CREATE2'd by this scheme) | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | - -**Method requirements**: - -- `eip3009`: requires the token to implement `receiveWithAuthorization` (ERC-3009). Examples: Circle USDC, EURC. -- `permit2`: works with any ERC-20. - -For the current list of chains where the canonical contracts are live and which stablecoins each chain's deploy supports, see [base/commerce-payments](https://github.com/base/commerce-payments). Facilitators on chains without a live deploy SHOULD reproduce the canonical bytecode using the source repo's pinned compiler / optimizer settings and broadcast at the salt labels above; the resulting addresses match the constants in the table by construction. From 2f259820d4f275c43e11d26fceb31759d5e2bb79 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 21:02:10 -0700 Subject: [PATCH 12/18] authCapture: trim assetTransferMethod cell, lift multi-accepts note 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) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 37c332a..4803e2e 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -57,7 +57,9 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | | `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | | `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | -| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. `eip3009` requires the token to implement `receiveWithAuthorization` (e.g., Circle USDC, EURC); `permit2` works with any ERC-20. A server MAY include multiple `accepts[]` entries with different `assetTransferMethod` values to let the client choose based on the tokens it holds. | +| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. | + +A server MAY list multiple `accepts[]` entries with different `assetTransferMethod` values so clients can pick the method matching their token approvals. **Universal contract addresses** (same on every supported EVM chain via deterministic CREATE2): From d59f8b57ed6aad9a041b74adbdabf3dec929d595 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 21:04:37 -0700 Subject: [PATCH 13/18] authCapture: keep multi-accepts hint in the assetTransferMethod cell Two sentences in a cell is fine; lifting it out and back in was overkill. Co-Authored-By: Claude Opus 4.7 (1M context) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 4803e2e..0201567 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -57,9 +57,7 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | | `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | | `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | -| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. | - -A server MAY list multiple `accepts[]` entries with different `assetTransferMethod` values so clients can pick the method matching their token approvals. +| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. A server MAY list multiple `accepts[]` entries with different `assetTransferMethod` values so clients can pick the method matching their token approvals. | **Universal contract addresses** (same on every supported EVM chain via deterministic CREATE2): From e85f2430a42a57a6b49af75b3c21e88440a6820c Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 21:14:13 -0700 Subject: [PATCH 14/18] authCapture: prettier table-column alignment Co-Authored-By: Claude Opus 4.7 (1M context) --- .../authCapture/scheme_authCapture_evm.md | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 0201567..e54c694 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -46,17 +46,17 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: ### `extra` Fields -| Field | Required | Type | Description | -| :-------------------- | :------- | :----------------------- | :-------------------------------------------------------------------------------------------------------- | -| `name` | Yes | `string` | EIP-712 token-domain name (e.g., `"USDC"`). Used for ERC-3009 signing only. | -| `version` | Yes | `string` | EIP-712 token-domain version (e.g., `"2"`). | -| `captureAuthorizer` | Yes | `address` | Address authorized to authorize/capture/void/refund/charge. Committed on-chain as `PaymentInfo.operator`. | -| `captureDeadline` | Yes | `uint48` | Absolute Unix seconds — capture must occur before this. Encoded as `authorizationExpiry`. | -| `refundDeadline` | Yes | `uint48` | Absolute Unix seconds — refunds allowed until this. Encoded as `refundExpiry`. | -| `feeRecipient` | Yes | `address` | Fee recipient (committed on-chain as `PaymentInfo.feeReceiver`). | -| `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | -| `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | -| `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | +| Field | Required | Type | Description | +| :-------------------- | :------- | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `name` | Yes | `string` | EIP-712 token-domain name (e.g., `"USDC"`). Used for ERC-3009 signing only. | +| `version` | Yes | `string` | EIP-712 token-domain version (e.g., `"2"`). | +| `captureAuthorizer` | Yes | `address` | Address authorized to authorize/capture/void/refund/charge. Committed on-chain as `PaymentInfo.operator`. | +| `captureDeadline` | Yes | `uint48` | Absolute Unix seconds — capture must occur before this. Encoded as `authorizationExpiry`. | +| `refundDeadline` | Yes | `uint48` | Absolute Unix seconds — refunds allowed until this. Encoded as `refundExpiry`. | +| `feeRecipient` | Yes | `address` | Fee recipient (committed on-chain as `PaymentInfo.feeReceiver`). | +| `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | +| `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | +| `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | | `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. A server MAY list multiple `accepts[]` entries with different `assetTransferMethod` values so clients can pick the method matching their token approvals. | **Universal contract addresses** (same on every supported EVM chain via deterministic CREATE2): @@ -110,16 +110,16 @@ The payload carries the signature and the client-generated `salt`. The facilitat **Field derivation (EIP-3009):** -| Payload field | Derived from | -| :-------------------------- | :---------------------------------------------------------------------------------------------------- | -| `authorization.from` | Client's own address | -| `authorization.to` | `EIP3009_TOKEN_COLLECTOR_ADDRESS` (universal constant) | -| `authorization.value` | `requirements.amount` | -| `authorization.validAfter` | `0` (the token collector hardcodes the lower bound) | +| Payload field | Derived from | +| :-------------------------- | :---------------------------------------------------------------------------------------------------------- | +| `authorization.from` | Client's own address | +| `authorization.to` | `EIP3009_TOKEN_COLLECTOR_ADDRESS` (universal constant) | +| `authorization.value` | `requirements.amount` | +| `authorization.validAfter` | `0` (the token collector hardcodes the lower bound) | | `authorization.validBefore` | `now + requirements.maxTimeoutSeconds` (also used as `preApprovalExpiry` when reconstructing `PaymentInfo`) | -| `authorization.nonce` | Payer-agnostic `PaymentInfo` hash — see [Nonce Derivation](#nonce-derivation-both-methods) | -| `salt` | Fresh `bytes32` generated client-side per signing call | -| EIP-712 domain | `{ name, version }` from `extra`; `chainId` from `network`; `verifyingContract = requirements.asset` | +| `authorization.nonce` | Payer-agnostic `PaymentInfo` hash — see [Nonce Derivation](#nonce-derivation-both-methods) | +| `salt` | Fresh `bytes32` generated client-side per signing call | +| EIP-712 domain | `{ name, version }` from `extra`; `chainId` from `network`; `verifyingContract = requirements.asset` | ### Permit2 @@ -147,16 +147,16 @@ The payload carries the signature and the client-generated `salt`. The facilitat **Field derivation (Permit2):** -| Payload field | Derived from | -| :--------------------------------------- | :---------------------------------------------------------------------------------------------------- | -| `permit2Authorization.from` | Client's own address | -| `permit2Authorization.permitted.token` | `requirements.asset` | -| `permit2Authorization.permitted.amount` | `requirements.amount` | -| `permit2Authorization.spender` | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (universal constant) | -| `permit2Authorization.nonce` | `uint256(payerAgnosticPaymentInfoHash)` — see [Nonce Derivation](#nonce-derivation-both-methods) | -| `permit2Authorization.deadline` | `now + requirements.maxTimeoutSeconds` (also used as `preApprovalExpiry` when reconstructing `PaymentInfo`) | -| `salt` | Fresh `bytes32` generated client-side per signing call | -| EIP-712 domain | Canonical Permit2 contract; `chainId` from `network` | +| Payload field | Derived from | +| :-------------------------------------- | :---------------------------------------------------------------------------------------------------------- | +| `permit2Authorization.from` | Client's own address | +| `permit2Authorization.permitted.token` | `requirements.asset` | +| `permit2Authorization.permitted.amount` | `requirements.amount` | +| `permit2Authorization.spender` | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (universal constant) | +| `permit2Authorization.nonce` | `uint256(payerAgnosticPaymentInfoHash)` — see [Nonce Derivation](#nonce-derivation-both-methods) | +| `permit2Authorization.deadline` | `now + requirements.maxTimeoutSeconds` (also used as `preApprovalExpiry` when reconstructing `PaymentInfo`) | +| `salt` | Fresh `bytes32` generated client-side per signing call | +| EIP-712 domain | Canonical Permit2 contract; `chainId` from `network` | **No witness** — the merchant address is bound through the deterministic nonce, not a separate witness struct. From 5de3a562d0fbe6f19795aa57db04724d784e42a0 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 21:34:33 -0700 Subject: [PATCH 15/18] authCapture: fix three review nits - 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) --- .../authCapture/scheme_authCapture_evm.md | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index e54c694..e8514ee 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -68,7 +68,7 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | | `PERMIT2_ADDRESS` (Uniswap canonical) | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | -See [Canonical Addresses](#canonical-addresses) for the deployed-chain list and the salt scheme. +See [Canonical Addresses](#canonical-addresses) for the salt scheme. ### Spec → on-chain field name mapping @@ -185,7 +185,7 @@ The facilitator performs these checks in order: 8. **Spender / collector match**: `payload.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `payload.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). 9. **Token match**: `payload.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). 10. **Signature verify**: Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `payer`. -11. **Amount**: Authorization value matches `requirements.amount`. +11. **Amount**: `authorization.value` (EIP-3009) or `permit2Authorization.permitted.amount` (Permit2) matches `requirements.amount`. 12. **Nonce match**: Reconstruct `PaymentInfo` from extra + payload.salt + payer + requirements; recompute payer-agnostic hash; assert it matches the wire nonce. 13. **Simulate** `AUTH_CAPTURE_ESCROW.authorize(...)` or `.charge(...)` to ensure success. @@ -209,26 +209,26 @@ The authCapture scheme uses the standard x402 error codes plus these scheme-spec ### Verification Errors -| Error Code | Description | -| :---------------------------------- | :----------------------------------------------------------------------- | -| `invalid_payload_format` | Payload doesn't match `Eip3009Payload` or `Permit2Payload`. | -| `unsupported_scheme` | Scheme is not `authCapture`. | -| `network_mismatch` | Payload network doesn't match requirements. | -| `invalid_network` | Network format is not `eip155:`. | -| `invalid_authCapture_extra` | Extra is missing required fields. | -| `unsupported_asset_transfer_method` | `assetTransferMethod` is not `"eip3009"` or `"permit2"`. | -| `payload_method_mismatch` | Payload shape doesn't match `assetTransferMethod`. | -| `capture_deadline_expired` | `captureDeadline <= now + 6s`. | -| `invalid_deadline_ordering` | `refundDeadline < captureDeadline`. | -| `authorization_expired` | EIP-3009 `validBefore` (or Permit2 `deadline`) `<= now + 6s`. | -| `authorization_not_yet_valid` | EIP-3009 `validAfter > now`. | -| `invalid_authCapture_signature` | Signature verification failed. | -| `amount_mismatch` | Authorization value doesn't match `requirements.amount`. | -| `token_collector_mismatch` | `to` / `spender` doesn't match the canonical collector for the method. | -| `token_mismatch` | Permit2 `permitted.token` doesn't match `requirements.asset`. | -| `nonce_mismatch` | Wire nonce doesn't match the recomputed payer-agnostic PaymentInfo hash. | -| `insufficient_balance` | Payer balance is less than required amount. | -| `simulation_failed` | Settlement simulation reverted with an unmapped error. | +| Error Code | Description | +| :---------------------------------- | :-------------------------------------------------------------------------------- | +| `invalid_payload_format` | Payload doesn't match `Eip3009Payload` or `Permit2Payload`. | +| `unsupported_scheme` | Scheme is not `authCapture`. | +| `network_mismatch` | Payload network doesn't match requirements. | +| `invalid_network` | Network format is not `eip155:`. | +| `invalid_authCapture_extra` | Extra is missing required fields. | +| `unsupported_asset_transfer_method` | `assetTransferMethod` is not `"eip3009"` or `"permit2"`. | +| `payload_method_mismatch` | Payload shape doesn't match `assetTransferMethod`. | +| `capture_deadline_expired` | `captureDeadline <= now + 6s`. | +| `invalid_deadline_ordering` | Deadlines violate `now + maxTimeoutSeconds <= captureDeadline <= refundDeadline`. | +| `authorization_expired` | EIP-3009 `validBefore` (or Permit2 `deadline`) `<= now + 6s`. | +| `authorization_not_yet_valid` | EIP-3009 `validAfter > now`. | +| `invalid_authCapture_signature` | Signature verification failed. | +| `amount_mismatch` | Authorization value doesn't match `requirements.amount`. | +| `token_collector_mismatch` | `to` / `spender` doesn't match the canonical collector for the method. | +| `token_mismatch` | Permit2 `permitted.token` doesn't match `requirements.asset`. | +| `nonce_mismatch` | Wire nonce doesn't match the recomputed payer-agnostic PaymentInfo hash. | +| `insufficient_balance` | Payer balance is less than required amount. | +| `simulation_failed` | Settlement simulation reverted with an unmapped error. | ### Typed simulation reverts From 3c305d54f11ecfacaca7519954b0146cf065d2c5 Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 21:37:55 -0700 Subject: [PATCH 16/18] authCapture: note transitive field equality from step 12 nonce match 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) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index e8514ee..4a6d26b 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -186,7 +186,7 @@ The facilitator performs these checks in order: 9. **Token match**: `payload.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). 10. **Signature verify**: Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `payer`. 11. **Amount**: `authorization.value` (EIP-3009) or `permit2Authorization.permitted.amount` (Permit2) matches `requirements.amount`. -12. **Nonce match**: Reconstruct `PaymentInfo` from extra + payload.salt + payer + requirements; recompute payer-agnostic hash; assert it matches the wire nonce. +12. **Nonce match**: Reconstruct `PaymentInfo` from extra + payload.salt + payer + requirements; recompute payer-agnostic hash; assert it matches the wire nonce. This transitively enforces equality on every field encoded in `PaymentInfo` (receiver, token, deadlines, fee bounds, feeRecipient), so individual field-by-field checks for those values are unnecessary. 13. **Simulate** `AUTH_CAPTURE_ESCROW.authorize(...)` or `.charge(...)` to ensure success. ### EIP-6492 Support From de84f9d949aa7a08aa0d2bb3465cf01cae78ca9c Mon Sep 17 00:00:00 2001 From: A1igator Date: Thu, 30 Apr 2026 21:55:10 -0700 Subject: [PATCH 17/18] authCapture: feeRecipient address(0) note + realistic Permit2 example nonce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- specs/schemes/authCapture/scheme_authCapture_evm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 4a6d26b..96cc351 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -53,7 +53,7 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `captureAuthorizer` | Yes | `address` | Address authorized to authorize/capture/void/refund/charge. Committed on-chain as `PaymentInfo.operator`. | | `captureDeadline` | Yes | `uint48` | Absolute Unix seconds — capture must occur before this. Encoded as `authorizationExpiry`. | | `refundDeadline` | Yes | `uint48` | Absolute Unix seconds — refunds allowed until this. Encoded as `refundExpiry`. | -| `feeRecipient` | Yes | `address` | Fee recipient (committed on-chain as `PaymentInfo.feeReceiver`). | +| `feeRecipient` | Yes | `address` | Fee recipient (committed on-chain as `PaymentInfo.feeReceiver`). Set to `address(0)` to let the captureAuthorizer specify any non-zero recipient at capture/charge time. | | `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | | `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | | `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | @@ -136,7 +136,7 @@ The payload carries the signature and the client-generated `salt`. The facilitat "amount": "1000000" }, "spender": "0xPermit2TokenCollectorAddress", - "nonce": "12345678901234567890", + "nonce": "110210486920734568342928534950928740912034856789012345678901234567890123456789", "deadline": "1740675754" }, "signature": "0x2d6a...571c", From a2735ba3b29b6ae252adea844bc73853eb194eff Mon Sep 17 00:00:00 2001 From: A1igator Date: Fri, 1 May 2026 15:19:10 -0700 Subject: [PATCH 18/18] authCapture: tighten facilitator-call wording and refund actor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- specs/schemes/authCapture/scheme_authCapture.md | 2 +- specs/schemes/authCapture/scheme_authCapture_evm.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index b4b86e3..836a4cc 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -21,7 +21,7 @@ The scheme supports two settlement paths, selected via `extra.autoCapture`: | `autoCapture` | Behavior | | :---------------- | :--------------------------------------------------------------------------------------------------------------------------- | | `false` (default) | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | -| `true` | Single-shot. Funds sent directly to receiver. Refundable post-settlement. | +| `true` | Single-shot. Funds sent directly to receiver. CaptureAuthorizer can refund post-settlement. | ### Two-phase (`autoCapture: false`, default) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 96cc351..55d8fee 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -10,7 +10,7 @@ The `authCapture` scheme on EVM uses the [base/commerce-payments](https://github - `PERMIT2_TOKEN_COLLECTOR_ADDRESS` — collects funds via Uniswap Permit2 `permitTransferFrom` (any ERC-20) - **`captureAuthorizer`**: Address authorized to authorize, capture, void, refund, or charge a payment. The escrow contract gates those operations on `msg.sender` matching this address. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.). -The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it to `AuthCaptureEscrow.authorize()` (two-phase) or `AuthCaptureEscrow.charge()` (single-shot via `autoCapture: true`). +The client signs a single signature (ERC-3009 or Permit2). The facilitator calls `AuthCaptureEscrow.authorize()` (two-phase) or `AuthCaptureEscrow.charge()` (single-shot via `autoCapture: true`), either directly or through a smart contract set as the captureAuthorizer. ## PaymentRequirements