From 1d27be296dc417260e45eb61b8493aadd63a744e Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Mon, 18 May 2026 16:05:21 -0700 Subject: [PATCH 01/10] docs: update authCapture EVM spec Document server-routed authCapture operations via payload type and remove autoCapture as the operation selector. Co-authored-by: Cursor --- .../schemes/authCapture/scheme_authCapture.md | 24 +++-- .../authCapture/scheme_authCapture_evm.md | 101 ++++++++++++++---- 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index 836a4cc947..cebba20765 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -2,7 +2,7 @@ ## 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). +`authCapture` is a payment scheme where funds can be held and settled later. The client authorizes a maximum amount, and the server or 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. @@ -16,14 +16,14 @@ Unlike `exact`, which has no built-in mechanism for returning funds, `authCaptur ## Settlement Paths -The scheme supports two settlement paths, selected via `extra.autoCapture`: +The scheme supports two settlement paths, selected by the operation `type` passed to the facilitator: -| `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. CaptureAuthorizer can refund post-settlement. | +| `type` | Behavior | +| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------- | +| `authorization` | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | +| `authorizeAndCapture` | Single-shot. Funds sent directly to receiver. CaptureAuthorizer can refund post-settlement. | -### Two-phase (`autoCapture: false`, default) +### Two-phase (`type: "authorization"`) ``` AUTHORIZE → RESOURCE DELIVERED → CAPTURE / VOID → (REFUND) @@ -35,7 +35,7 @@ AUTHORIZE → RESOURCE DELIVERED → CAPTURE / VOID → (REFUND) 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`) +### Single-shot (`type: "authorizeAndCapture"`) ``` CHARGE → RESOURCE DELIVERED → (REFUND) @@ -47,6 +47,14 @@ CHARGE → RESOURCE DELIVERED → (REFUND) No capture, void, or reclaim — funds are never held in escrow. +## Server Operations + +Facilitators MUST provide a mechanism for servers to perform `authorization`, `authorizeAndCapture`, `capture`, `void`, and `refund` operations. Servers select the operation by passing a `type` field to the facilitator's `verify` and `settle` endpoints. Network bindings define the payload fields required for each operation. + +Servers MAY self-facilitate by interacting with the escrow contract or network-specific settlement mechanism directly instead of using a third-party facilitator. + +Facilitators MAY require proof that the server controls the signed authorization's `payTo` address before performing server-initiated operations. Network bindings may define a `serverAuthorization` field for this purpose. For EVM, `serverAuthorization` is an identity proof over the payment's derived nonce signed by `payTo`. + ## Core Properties ### Fund Safety diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 888e086ba8..9d3477e1dd 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 calls `AuthCaptureEscrow.authorize()` (two-phase) or `AuthCaptureEscrow.charge()` (single-shot via `autoCapture: true`), either directly or through a smart contract set as the captureAuthorizer. +The client signs a single signature (ERC-3009 or Permit2). The server chooses the operation by passing a payload `type` to the facilitator's `verify` and `settle` endpoints. The facilitator calls `AuthCaptureEscrow.authorize()` for `authorization`, `AuthCaptureEscrow.charge()` for `authorizeAndCapture`, or the matching follow-up function for `capture`, `void`, and `refund`, either directly or through a smart contract set as the captureAuthorizer. ## PaymentRequirements @@ -36,7 +36,6 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: "minFeeBps": 0, "maxFeeBps": 1000, "feeRecipient": "0xFeeRecipientAddress", - "autoCapture": false, "assetTransferMethod": "eip3009" } } @@ -56,7 +55,6 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `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`. | | `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. | ### Spec → on-chain field name mapping @@ -73,7 +71,17 @@ The wire-format extra uses spec-level field names. The on-chain `PaymentInfo` st ## 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`). +The server-to-facilitator payload carries a `type` discriminator. For `authorization` and `authorizeAndCapture`, it also carries the client token authorization signature and client-generated `salt`. The facilitator reconstructs or validates the full `PaymentInfo` from `extra` + `salt` + payer + top-level requirements (`payTo`, `asset`, `amount`). + +| `payload.type` | Required fields | Escrow call | +| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------- | +| `authorization` | token authorization payload, `signature`, `salt`, `paymentInfo`, optional `serverAuthorization` | `authorize()` | +| `authorizeAndCapture` | token authorization payload, `signature`, `salt`, `paymentInfo`, `capture.amount`, `capture.feeBps`, `capture.feeRecipient`, optional `serverAuthorization` | `charge()` | +| `capture` | `paymentInfo`, `amount`, `feeBps`, `feeRecipient`, optional `serverAuthorization` | `capture()` | +| `void` | `paymentInfo`, optional `serverAuthorization` | `void()` | +| `refund` | `paymentInfo`, `amount`, optional `serverAuthorization` | `refund()` | + +`authorization` is the two-phase path: funds are held in escrow and can later be captured, voided, reclaimed, or refunded. `authorizeAndCapture` is the single-shot path: funds are sent directly to the receiver with refund capability. ### EIP-3009 (default) @@ -83,6 +91,7 @@ The payload carries the signature and the client-generated `salt`. The facilitat "resource": { "url": "https://api.example.com/resource", "method": "GET" }, "accepted": { "scheme": "authCapture", "...": "..." }, "payload": { + "type": "authorization", "authorization": { "from": "0xPayerAddress", "to": "0xEIP3009TokenCollectorAddress", @@ -92,7 +101,8 @@ The payload carries the signature and the client-generated `salt`. The facilitat "nonce": "0xf374...3480" }, "signature": "0x2d6a...571c", - "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc", + "paymentInfo": { "...": "..." } } } ``` @@ -118,6 +128,7 @@ The payload carries the signature and the client-generated `salt`. The facilitat "resource": { "url": "https://api.example.com/resource", "method": "GET" }, "accepted": { "scheme": "authCapture", "...": "..." }, "payload": { + "type": "authorization", "permit2Authorization": { "from": "0xPayerAddress", "permitted": { @@ -129,7 +140,8 @@ The payload carries the signature and the client-generated `salt`. The facilitat "deadline": "1740675754" }, "signature": "0x2d6a...571c", - "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc", + "paymentInfo": { "...": "..." } } } ``` @@ -160,23 +172,52 @@ nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, pay 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. +### Server Authorization + +A facilitator MAY require `payload.serverAuthorization` before accepting a server-initiated operation. `serverAuthorization` is an identity proof that the server controls `requirements.payTo` / `paymentInfo.receiver`; it does not authorize a specific operation or amount. + +```json +{ + "serverAuthorization": { + "signature": "0xServerSignature" + } +} +``` + +The server signs an EIP-712 `ServerAuthorization` message over the derived payment nonce: + +```solidity +ServerAuthorization(bytes32 nonce) +``` + +The EIP-712 domain is `{ name: "x402 AuthCapture", version: "1", chainId, verifyingContract: AUTH_CAPTURE_ESCROW_ADDRESS }`. The `nonce` is the same derived nonce used by the client authorization: + +``` +nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash)) +``` + +Facilitators verify EOA `payTo` addresses with ECDSA recovery and contract `payTo` addresses with EIP-1271. If a facilitator requires server identity proof, it MUST reject the request when `serverAuthorization` is missing or does not verify against `requirements.payTo` / `paymentInfo.receiver`. + ## 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`). +1. **Type guard**: Verify `payload.type` is one of `authorization`, `authorizeAndCapture`, `capture`, `void`, or `refund`, and that the payload includes the fields required for that operation. 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`, `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`. -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` (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. 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. +5. **PaymentInfo match**: Verify `paymentInfo.operator === extra.captureAuthorizer`, `paymentInfo.receiver === requirements.payTo`, `paymentInfo.token === requirements.asset`, `paymentInfo.maxAmount === requirements.amount`, deadlines and fee fields match `extra`, and `paymentInfo.preApprovalExpiry` is derived from `maxTimeoutSeconds`. +6. **Server authorization**: If required by the facilitator, verify `payload.serverAuthorization` as an EIP-712 identity proof from `requirements.payTo` / `paymentInfo.receiver`. +7. **Method routing** (`authorization`, `authorizeAndCapture` only): `extra.assetTransferMethod` (default `"eip3009"`) matches the token authorization payload shape. +8. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and for `authorization` / `authorizeAndCapture`, `authorization.validBefore` (EIP-3009) / `permit2Authorization.deadline` (Permit2) `<= captureDeadline`. +9. **Time window** (`authorization`, `authorizeAndCapture` only): `authorization.validBefore` / `permit2Authorization.deadline > now + 6s` (not expired) and `authorization.validAfter <= now` (active, EIP-3009 only). +10. **Spender / collector match** (`authorization`, `authorizeAndCapture` only): `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). +11. **Token match** (`authorization`, `authorizeAndCapture` only): `permit2Authorization.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). +12. **Signature verify** (`authorization`, `authorizeAndCapture` only): Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `paymentInfo.payer`. +13. **Amount** (`authorization`, `authorizeAndCapture` only): `authorization.value` (EIP-3009) or `permit2Authorization.permitted.amount` (Permit2) matches `requirements.amount`. +14. **Nonce match** (`authorization`, `authorizeAndCapture` only): Recompute the payer-agnostic hash from `paymentInfo` with payer zeroed; 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. +15. **Operation validity**: Verify the requested operation is valid for the current on-chain payment state. `capture` and `void` require an existing authorization. `refund` requires an existing captured or charged payment within the refund window. +16. **Simulate** the mapped escrow call to ensure success. ### EIP-6492 Support @@ -185,12 +226,23 @@ For smart wallet clients, the signature may be EIP-6492 wrapped (containing depl ## 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)`. +2. **Determine function** from `payload.type`: + +| `payload.type` | Function | Notes | +| :-------------------- | :------------------------------- | :-------------------------------------------------------------------- | +| `authorization` | `AuthCaptureEscrow.authorize()` | Collects tokens into escrow for later capture or void. | +| `authorizeAndCapture` | `AuthCaptureEscrow.charge()` | Collects tokens and transfers the captured amount in one transaction. | +| `capture` | `AuthCaptureEscrow.capture()` | Captures an existing authorization. | +| `void` | `AuthCaptureEscrow.void()` | Releases an existing authorization back to the payer. | +| `refund` | `AuthCaptureEscrow.refund()` | Refunds a captured or charged payment. | + +3. **Resolve collector** (`authorization`, `authorizeAndCapture` only): `EIP3009_TOKEN_COLLECTOR_ADDRESS` or `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (per `assetTransferMethod`). +4. **Encode `collectorData`** (`authorization`, `authorizeAndCapture` only): raw ERC-3009 signature, or ABI-encoded Permit2 signature. +5. **Call escrow** with the operation-specific fields from the payload. 6. **Wait for receipt**: 60s timeout. -7. **Return result**: tx hash, network, payer. +7. **Return result**: tx hash, network, payer, and settled or refunded amount where applicable. + +Servers that self-facilitate perform the same escrow calls directly instead of relaying the operation through a third-party facilitator. In that mode, the server is responsible for the same verification rules, payment state checks, and transaction submission behavior described above. ## Error Codes @@ -200,11 +252,15 @@ The authCapture scheme uses the standard x402 error codes plus these scheme-spec | Error Code | Description | | :---------------------------------- | :-------------------------------------------------------------------------------- | -| `invalid_payload_format` | Payload doesn't match `Eip3009Payload` or `Permit2Payload`. | +| `invalid_payload_format` | Payload doesn't match the fields required for its `type`. | +| `invalid_operation_type` | `payload.type` is not one of the supported authCapture operations. | | `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. | +| `payment_info_mismatch` | `paymentInfo` does not match the accepted payment requirements. | +| `missing_server_authorization` | Facilitator requires `serverAuthorization`, but the payload omitted it. | +| `invalid_server_authorization` | `serverAuthorization` does not verify against `requirements.payTo`. | | `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`. | @@ -216,6 +272,7 @@ The authCapture scheme uses the standard x402 error codes plus these scheme-spec | `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. | +| `invalid_operation_state` | Requested operation is not valid for the current on-chain payment state. | | `insufficient_balance` | Payer balance is less than required amount. | | `simulation_failed` | Settlement simulation reverted with an unmapped error. | From e59a20b864fbb5d50b613e364f37dd36caf6eecf Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Mon, 18 May 2026 16:08:24 -0700 Subject: [PATCH 02/10] docs: rename authCapture authorize operation type Use authorize as the server operation discriminator while preserving authorization for token payload fields. Co-authored-by: Cursor --- .../schemes/authCapture/scheme_authCapture.md | 6 ++-- .../authCapture/scheme_authCapture_evm.md | 36 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index cebba20765..4cb6e3acff 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -20,10 +20,10 @@ The scheme supports two settlement paths, selected by the operation `type` passe | `type` | Behavior | | :-------------------- | :--------------------------------------------------------------------------------------------------------------------------- | -| `authorization` | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | +| `authorize` | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | | `authorizeAndCapture` | Single-shot. Funds sent directly to receiver. CaptureAuthorizer can refund post-settlement. | -### Two-phase (`type: "authorization"`) +### Two-phase (`type: "authorize"`) ``` AUTHORIZE → RESOURCE DELIVERED → CAPTURE / VOID → (REFUND) @@ -49,7 +49,7 @@ No capture, void, or reclaim — funds are never held in escrow. ## Server Operations -Facilitators MUST provide a mechanism for servers to perform `authorization`, `authorizeAndCapture`, `capture`, `void`, and `refund` operations. Servers select the operation by passing a `type` field to the facilitator's `verify` and `settle` endpoints. Network bindings define the payload fields required for each operation. +Facilitators MUST provide a mechanism for servers to perform `authorize`, `authorizeAndCapture`, `capture`, `void`, and `refund` operations. Servers select the operation by passing a `type` field to the facilitator's `verify` and `settle` endpoints. Network bindings define the payload fields required for each operation. Servers MAY self-facilitate by interacting with the escrow contract or network-specific settlement mechanism directly instead of using a third-party facilitator. diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 9d3477e1dd..3511c1097c 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 server chooses the operation by passing a payload `type` to the facilitator's `verify` and `settle` endpoints. The facilitator calls `AuthCaptureEscrow.authorize()` for `authorization`, `AuthCaptureEscrow.charge()` for `authorizeAndCapture`, or the matching follow-up function for `capture`, `void`, and `refund`, either directly or through a smart contract set as the captureAuthorizer. +The client signs a single signature (ERC-3009 or Permit2). The server chooses the operation by passing a payload `type` to the facilitator's `verify` and `settle` endpoints. The facilitator calls `AuthCaptureEscrow.authorize()` for `authorize`, `AuthCaptureEscrow.charge()` for `authorizeAndCapture`, or the matching follow-up function for `capture`, `void`, and `refund`, either directly or through a smart contract set as the captureAuthorizer. ## PaymentRequirements @@ -71,17 +71,17 @@ The wire-format extra uses spec-level field names. The on-chain `PaymentInfo` st ## PaymentPayload -The server-to-facilitator payload carries a `type` discriminator. For `authorization` and `authorizeAndCapture`, it also carries the client token authorization signature and client-generated `salt`. The facilitator reconstructs or validates the full `PaymentInfo` from `extra` + `salt` + payer + top-level requirements (`payTo`, `asset`, `amount`). +The server-to-facilitator payload carries a `type` discriminator. For `authorize` and `authorizeAndCapture`, it also carries the client token authorization signature and client-generated `salt`. The facilitator reconstructs or validates the full `PaymentInfo` from `extra` + `salt` + payer + top-level requirements (`payTo`, `asset`, `amount`). | `payload.type` | Required fields | Escrow call | | :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------- | -| `authorization` | token authorization payload, `signature`, `salt`, `paymentInfo`, optional `serverAuthorization` | `authorize()` | +| `authorize` | token authorization payload, `signature`, `salt`, `paymentInfo`, optional `serverAuthorization` | `authorize()` | | `authorizeAndCapture` | token authorization payload, `signature`, `salt`, `paymentInfo`, `capture.amount`, `capture.feeBps`, `capture.feeRecipient`, optional `serverAuthorization` | `charge()` | | `capture` | `paymentInfo`, `amount`, `feeBps`, `feeRecipient`, optional `serverAuthorization` | `capture()` | | `void` | `paymentInfo`, optional `serverAuthorization` | `void()` | | `refund` | `paymentInfo`, `amount`, optional `serverAuthorization` | `refund()` | -`authorization` is the two-phase path: funds are held in escrow and can later be captured, voided, reclaimed, or refunded. `authorizeAndCapture` is the single-shot path: funds are sent directly to the receiver with refund capability. +`authorize` is the two-phase path: funds are held in escrow and can later be captured, voided, reclaimed, or refunded. `authorizeAndCapture` is the single-shot path: funds are sent directly to the receiver with refund capability. ### EIP-3009 (default) @@ -91,7 +91,7 @@ The server-to-facilitator payload carries a `type` discriminator. For `authoriza "resource": { "url": "https://api.example.com/resource", "method": "GET" }, "accepted": { "scheme": "authCapture", "...": "..." }, "payload": { - "type": "authorization", + "type": "authorize", "authorization": { "from": "0xPayerAddress", "to": "0xEIP3009TokenCollectorAddress", @@ -128,7 +128,7 @@ The server-to-facilitator payload carries a `type` discriminator. For `authoriza "resource": { "url": "https://api.example.com/resource", "method": "GET" }, "accepted": { "scheme": "authCapture", "...": "..." }, "payload": { - "type": "authorization", + "type": "authorize", "permit2Authorization": { "from": "0xPayerAddress", "permitted": { @@ -202,20 +202,20 @@ Facilitators verify EOA `payTo` addresses with ECDSA recovery and contract `payT The facilitator performs these checks in order: -1. **Type guard**: Verify `payload.type` is one of `authorization`, `authorizeAndCapture`, `capture`, `void`, or `refund`, and that the payload includes the fields required for that operation. +1. **Type guard**: Verify `payload.type` is one of `authorize`, `authorizeAndCapture`, `capture`, `void`, or `refund`, and that the payload includes the fields required for that operation. 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`, `minFeeBps`, `maxFeeBps`, `name`, `version`). 5. **PaymentInfo match**: Verify `paymentInfo.operator === extra.captureAuthorizer`, `paymentInfo.receiver === requirements.payTo`, `paymentInfo.token === requirements.asset`, `paymentInfo.maxAmount === requirements.amount`, deadlines and fee fields match `extra`, and `paymentInfo.preApprovalExpiry` is derived from `maxTimeoutSeconds`. 6. **Server authorization**: If required by the facilitator, verify `payload.serverAuthorization` as an EIP-712 identity proof from `requirements.payTo` / `paymentInfo.receiver`. -7. **Method routing** (`authorization`, `authorizeAndCapture` only): `extra.assetTransferMethod` (default `"eip3009"`) matches the token authorization payload shape. -8. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and for `authorization` / `authorizeAndCapture`, `authorization.validBefore` (EIP-3009) / `permit2Authorization.deadline` (Permit2) `<= captureDeadline`. -9. **Time window** (`authorization`, `authorizeAndCapture` only): `authorization.validBefore` / `permit2Authorization.deadline > now + 6s` (not expired) and `authorization.validAfter <= now` (active, EIP-3009 only). -10. **Spender / collector match** (`authorization`, `authorizeAndCapture` only): `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). -11. **Token match** (`authorization`, `authorizeAndCapture` only): `permit2Authorization.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). -12. **Signature verify** (`authorization`, `authorizeAndCapture` only): Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `paymentInfo.payer`. -13. **Amount** (`authorization`, `authorizeAndCapture` only): `authorization.value` (EIP-3009) or `permit2Authorization.permitted.amount` (Permit2) matches `requirements.amount`. -14. **Nonce match** (`authorization`, `authorizeAndCapture` only): Recompute the payer-agnostic hash from `paymentInfo` with payer zeroed; 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. +7. **Method routing** (`authorize`, `authorizeAndCapture` only): `extra.assetTransferMethod` (default `"eip3009"`) matches the token authorization payload shape. +8. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and for `authorize` / `authorizeAndCapture`, `authorization.validBefore` (EIP-3009) / `permit2Authorization.deadline` (Permit2) `<= captureDeadline`. +9. **Time window** (`authorize`, `authorizeAndCapture` only): `authorization.validBefore` / `permit2Authorization.deadline > now + 6s` (not expired) and `authorization.validAfter <= now` (active, EIP-3009 only). +10. **Spender / collector match** (`authorize`, `authorizeAndCapture` only): `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). +11. **Token match** (`authorize`, `authorizeAndCapture` only): `permit2Authorization.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). +12. **Signature verify** (`authorize`, `authorizeAndCapture` only): Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `paymentInfo.payer`. +13. **Amount** (`authorize`, `authorizeAndCapture` only): `authorization.value` (EIP-3009) or `permit2Authorization.permitted.amount` (Permit2) matches `requirements.amount`. +14. **Nonce match** (`authorize`, `authorizeAndCapture` only): Recompute the payer-agnostic hash from `paymentInfo` with payer zeroed; 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. 15. **Operation validity**: Verify the requested operation is valid for the current on-chain payment state. `capture` and `void` require an existing authorization. `refund` requires an existing captured or charged payment within the refund window. 16. **Simulate** the mapped escrow call to ensure success. @@ -230,14 +230,14 @@ For smart wallet clients, the signature may be EIP-6492 wrapped (containing depl | `payload.type` | Function | Notes | | :-------------------- | :------------------------------- | :-------------------------------------------------------------------- | -| `authorization` | `AuthCaptureEscrow.authorize()` | Collects tokens into escrow for later capture or void. | +| `authorize` | `AuthCaptureEscrow.authorize()` | Collects tokens into escrow for later capture or void. | | `authorizeAndCapture` | `AuthCaptureEscrow.charge()` | Collects tokens and transfers the captured amount in one transaction. | | `capture` | `AuthCaptureEscrow.capture()` | Captures an existing authorization. | | `void` | `AuthCaptureEscrow.void()` | Releases an existing authorization back to the payer. | | `refund` | `AuthCaptureEscrow.refund()` | Refunds a captured or charged payment. | -3. **Resolve collector** (`authorization`, `authorizeAndCapture` only): `EIP3009_TOKEN_COLLECTOR_ADDRESS` or `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (per `assetTransferMethod`). -4. **Encode `collectorData`** (`authorization`, `authorizeAndCapture` only): raw ERC-3009 signature, or ABI-encoded Permit2 signature. +3. **Resolve collector** (`authorize`, `authorizeAndCapture` only): `EIP3009_TOKEN_COLLECTOR_ADDRESS` or `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (per `assetTransferMethod`). +4. **Encode `collectorData`** (`authorize`, `authorizeAndCapture` only): raw ERC-3009 signature, or ABI-encoded Permit2 signature. 5. **Call escrow** with the operation-specific fields from the payload. 6. **Wait for receipt**: 60s timeout. 7. **Return result**: tx hash, network, payer, and settled or refunded amount where applicable. From 24a99a1c8fb8d4540214f973abbceee8268e551b Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Mon, 18 May 2026 16:10:56 -0700 Subject: [PATCH 03/10] docs: signal required authCapture server authorization Add serverAuthorizationRequired to advertised authCapture requirements when facilitators require server identity proof. Co-authored-by: Cursor --- specs/schemes/authCapture/scheme_authCapture.md | 2 +- specs/schemes/authCapture/scheme_authCapture_evm.md | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index 4cb6e3acff..e0f418820a 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -53,7 +53,7 @@ Facilitators MUST provide a mechanism for servers to perform `authorize`, `autho Servers MAY self-facilitate by interacting with the escrow contract or network-specific settlement mechanism directly instead of using a third-party facilitator. -Facilitators MAY require proof that the server controls the signed authorization's `payTo` address before performing server-initiated operations. Network bindings may define a `serverAuthorization` field for this purpose. For EVM, `serverAuthorization` is an identity proof over the payment's derived nonce signed by `payTo`. +Facilitators MAY require proof that the server controls the signed authorization's `payTo` address before performing server-initiated operations. Facilitators that require this proof MUST signal it with `extra.serverAuthorizationRequired` in the payment requirements. Network bindings may define a `serverAuthorization` field for this purpose. For EVM, `serverAuthorization` is an identity proof over the payment's derived nonce signed by `payTo`. ## Core Properties diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 3511c1097c..9875a93522 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -36,6 +36,7 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: "minFeeBps": 0, "maxFeeBps": 1000, "feeRecipient": "0xFeeRecipientAddress", + "serverAuthorizationRequired": true, "assetTransferMethod": "eip3009" } } @@ -55,6 +56,7 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: | `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). | +| `serverAuthorizationRequired` | No | `bool` | Whether the facilitator requires `payload.serverAuthorization` before accepting server-initiated operations. 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. | ### Spec → on-chain field name mapping @@ -174,7 +176,7 @@ Freshness is enforced by `salt`: each signing call generates a fresh `bytes32` s ### Server Authorization -A facilitator MAY require `payload.serverAuthorization` before accepting a server-initiated operation. `serverAuthorization` is an identity proof that the server controls `requirements.payTo` / `paymentInfo.receiver`; it does not authorize a specific operation or amount. +A facilitator MAY require `payload.serverAuthorization` before accepting a server-initiated operation. Facilitators that require it MUST set `extra.serverAuthorizationRequired: true` in the payment requirements. `serverAuthorization` is an identity proof that the server controls `requirements.payTo` / `paymentInfo.receiver`; it does not authorize a specific operation or amount. ```json { @@ -196,7 +198,7 @@ The EIP-712 domain is `{ name: "x402 AuthCapture", version: "1", chainId, verify nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash)) ``` -Facilitators verify EOA `payTo` addresses with ECDSA recovery and contract `payTo` addresses with EIP-1271. If a facilitator requires server identity proof, it MUST reject the request when `serverAuthorization` is missing or does not verify against `requirements.payTo` / `paymentInfo.receiver`. +Facilitators verify EOA `payTo` addresses with ECDSA recovery and contract `payTo` addresses with EIP-1271. If `extra.serverAuthorizationRequired` is `true`, the facilitator MUST reject the request when `serverAuthorization` is missing or does not verify against `requirements.payTo` / `paymentInfo.receiver`. ## Verification Logic @@ -207,7 +209,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. **PaymentInfo match**: Verify `paymentInfo.operator === extra.captureAuthorizer`, `paymentInfo.receiver === requirements.payTo`, `paymentInfo.token === requirements.asset`, `paymentInfo.maxAmount === requirements.amount`, deadlines and fee fields match `extra`, and `paymentInfo.preApprovalExpiry` is derived from `maxTimeoutSeconds`. -6. **Server authorization**: If required by the facilitator, verify `payload.serverAuthorization` as an EIP-712 identity proof from `requirements.payTo` / `paymentInfo.receiver`. +6. **Server authorization**: If `extra.serverAuthorizationRequired` is `true`, verify `payload.serverAuthorization` as an EIP-712 identity proof from `requirements.payTo` / `paymentInfo.receiver`. 7. **Method routing** (`authorize`, `authorizeAndCapture` only): `extra.assetTransferMethod` (default `"eip3009"`) matches the token authorization payload shape. 8. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and for `authorize` / `authorizeAndCapture`, `authorization.validBefore` (EIP-3009) / `permit2Authorization.deadline` (Permit2) `<= captureDeadline`. 9. **Time window** (`authorize`, `authorizeAndCapture` only): `authorization.validBefore` / `permit2Authorization.deadline > now + 6s` (not expired) and `authorization.validAfter <= now` (active, EIP-3009 only). From 75c6e5baae15d8223450056161f17ed4f035e1c7 Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Wed, 20 May 2026 08:41:45 -0700 Subject: [PATCH 04/10] docs: rename authCapture charge operation type Use charge for the single-shot authCapture operation instead of authorizeAndCapture. Co-authored-by: Cursor --- .../schemes/authCapture/scheme_authCapture.md | 6 ++-- .../authCapture/scheme_authCapture_evm.md | 32 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index e0f418820a..3b23612c79 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 by the operation `type` passe | `type` | Behavior | | :-------------------- | :--------------------------------------------------------------------------------------------------------------------------- | | `authorize` | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | -| `authorizeAndCapture` | Single-shot. Funds sent directly to receiver. CaptureAuthorizer can refund post-settlement. | +| `charge` | Single-shot. Funds sent directly to receiver. CaptureAuthorizer can refund post-settlement. | ### Two-phase (`type: "authorize"`) @@ -35,7 +35,7 @@ AUTHORIZE → RESOURCE DELIVERED → CAPTURE / VOID → (REFUND) 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 (`type: "authorizeAndCapture"`) +### Single-shot (`type: "charge"`) ``` CHARGE → RESOURCE DELIVERED → (REFUND) @@ -49,7 +49,7 @@ No capture, void, or reclaim — funds are never held in escrow. ## Server Operations -Facilitators MUST provide a mechanism for servers to perform `authorize`, `authorizeAndCapture`, `capture`, `void`, and `refund` operations. Servers select the operation by passing a `type` field to the facilitator's `verify` and `settle` endpoints. Network bindings define the payload fields required for each operation. +Facilitators MUST provide a mechanism for servers to perform `authorize`, `charge`, `capture`, `void`, and `refund` operations. Servers select the operation by passing a `type` field to the facilitator's `verify` and `settle` endpoints. Network bindings define the payload fields required for each operation. Servers MAY self-facilitate by interacting with the escrow contract or network-specific settlement mechanism directly instead of using a third-party facilitator. diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 9875a93522..3b9e9cd77d 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 server chooses the operation by passing a payload `type` to the facilitator's `verify` and `settle` endpoints. The facilitator calls `AuthCaptureEscrow.authorize()` for `authorize`, `AuthCaptureEscrow.charge()` for `authorizeAndCapture`, or the matching follow-up function for `capture`, `void`, and `refund`, either directly or through a smart contract set as the captureAuthorizer. +The client signs a single signature (ERC-3009 or Permit2). The server chooses the operation by passing a payload `type` to the facilitator's `verify` and `settle` endpoints. The facilitator calls `AuthCaptureEscrow.authorize()` for `authorize`, `AuthCaptureEscrow.charge()` for `charge`, or the matching follow-up function for `capture`, `void`, and `refund`, either directly or through a smart contract set as the captureAuthorizer. ## PaymentRequirements @@ -73,17 +73,17 @@ The wire-format extra uses spec-level field names. The on-chain `PaymentInfo` st ## PaymentPayload -The server-to-facilitator payload carries a `type` discriminator. For `authorize` and `authorizeAndCapture`, it also carries the client token authorization signature and client-generated `salt`. The facilitator reconstructs or validates the full `PaymentInfo` from `extra` + `salt` + payer + top-level requirements (`payTo`, `asset`, `amount`). +The server-to-facilitator payload carries a `type` discriminator. For `authorize` and `charge`, it also carries the client token authorization signature and client-generated `salt`. The facilitator reconstructs or validates the full `PaymentInfo` from `extra` + `salt` + payer + top-level requirements (`payTo`, `asset`, `amount`). | `payload.type` | Required fields | Escrow call | | :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------- | | `authorize` | token authorization payload, `signature`, `salt`, `paymentInfo`, optional `serverAuthorization` | `authorize()` | -| `authorizeAndCapture` | token authorization payload, `signature`, `salt`, `paymentInfo`, `capture.amount`, `capture.feeBps`, `capture.feeRecipient`, optional `serverAuthorization` | `charge()` | +| `charge` | token authorization payload, `signature`, `salt`, `paymentInfo`, `capture.amount`, `capture.feeBps`, `capture.feeRecipient`, optional `serverAuthorization` | `charge()` | | `capture` | `paymentInfo`, `amount`, `feeBps`, `feeRecipient`, optional `serverAuthorization` | `capture()` | | `void` | `paymentInfo`, optional `serverAuthorization` | `void()` | | `refund` | `paymentInfo`, `amount`, optional `serverAuthorization` | `refund()` | -`authorize` is the two-phase path: funds are held in escrow and can later be captured, voided, reclaimed, or refunded. `authorizeAndCapture` is the single-shot path: funds are sent directly to the receiver with refund capability. +`authorize` is the two-phase path: funds are held in escrow and can later be captured, voided, reclaimed, or refunded. `charge` is the single-shot path: funds are sent directly to the receiver with refund capability. ### EIP-3009 (default) @@ -204,20 +204,20 @@ Facilitators verify EOA `payTo` addresses with ECDSA recovery and contract `payT The facilitator performs these checks in order: -1. **Type guard**: Verify `payload.type` is one of `authorize`, `authorizeAndCapture`, `capture`, `void`, or `refund`, and that the payload includes the fields required for that operation. +1. **Type guard**: Verify `payload.type` is one of `authorize`, `charge`, `capture`, `void`, or `refund`, and that the payload includes the fields required for that operation. 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`, `minFeeBps`, `maxFeeBps`, `name`, `version`). 5. **PaymentInfo match**: Verify `paymentInfo.operator === extra.captureAuthorizer`, `paymentInfo.receiver === requirements.payTo`, `paymentInfo.token === requirements.asset`, `paymentInfo.maxAmount === requirements.amount`, deadlines and fee fields match `extra`, and `paymentInfo.preApprovalExpiry` is derived from `maxTimeoutSeconds`. 6. **Server authorization**: If `extra.serverAuthorizationRequired` is `true`, verify `payload.serverAuthorization` as an EIP-712 identity proof from `requirements.payTo` / `paymentInfo.receiver`. -7. **Method routing** (`authorize`, `authorizeAndCapture` only): `extra.assetTransferMethod` (default `"eip3009"`) matches the token authorization payload shape. -8. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and for `authorize` / `authorizeAndCapture`, `authorization.validBefore` (EIP-3009) / `permit2Authorization.deadline` (Permit2) `<= captureDeadline`. -9. **Time window** (`authorize`, `authorizeAndCapture` only): `authorization.validBefore` / `permit2Authorization.deadline > now + 6s` (not expired) and `authorization.validAfter <= now` (active, EIP-3009 only). -10. **Spender / collector match** (`authorize`, `authorizeAndCapture` only): `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). -11. **Token match** (`authorize`, `authorizeAndCapture` only): `permit2Authorization.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). -12. **Signature verify** (`authorize`, `authorizeAndCapture` only): Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `paymentInfo.payer`. -13. **Amount** (`authorize`, `authorizeAndCapture` only): `authorization.value` (EIP-3009) or `permit2Authorization.permitted.amount` (Permit2) matches `requirements.amount`. -14. **Nonce match** (`authorize`, `authorizeAndCapture` only): Recompute the payer-agnostic hash from `paymentInfo` with payer zeroed; 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. +7. **Method routing** (`authorize`, `charge` only): `extra.assetTransferMethod` (default `"eip3009"`) matches the token authorization payload shape. +8. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and for `authorize` / `charge`, `authorization.validBefore` (EIP-3009) / `permit2Authorization.deadline` (Permit2) `<= captureDeadline`. +9. **Time window** (`authorize`, `charge` only): `authorization.validBefore` / `permit2Authorization.deadline > now + 6s` (not expired) and `authorization.validAfter <= now` (active, EIP-3009 only). +10. **Spender / collector match** (`authorize`, `charge` only): `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). +11. **Token match** (`authorize`, `charge` only): `permit2Authorization.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). +12. **Signature verify** (`authorize`, `charge` only): Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `paymentInfo.payer`. +13. **Amount** (`authorize`, `charge` only): `authorization.value` (EIP-3009) or `permit2Authorization.permitted.amount` (Permit2) matches `requirements.amount`. +14. **Nonce match** (`authorize`, `charge` only): Recompute the payer-agnostic hash from `paymentInfo` with payer zeroed; 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. 15. **Operation validity**: Verify the requested operation is valid for the current on-chain payment state. `capture` and `void` require an existing authorization. `refund` requires an existing captured or charged payment within the refund window. 16. **Simulate** the mapped escrow call to ensure success. @@ -233,13 +233,13 @@ For smart wallet clients, the signature may be EIP-6492 wrapped (containing depl | `payload.type` | Function | Notes | | :-------------------- | :------------------------------- | :-------------------------------------------------------------------- | | `authorize` | `AuthCaptureEscrow.authorize()` | Collects tokens into escrow for later capture or void. | -| `authorizeAndCapture` | `AuthCaptureEscrow.charge()` | Collects tokens and transfers the captured amount in one transaction. | +| `charge` | `AuthCaptureEscrow.charge()` | Collects tokens and transfers the captured amount in one transaction. | | `capture` | `AuthCaptureEscrow.capture()` | Captures an existing authorization. | | `void` | `AuthCaptureEscrow.void()` | Releases an existing authorization back to the payer. | | `refund` | `AuthCaptureEscrow.refund()` | Refunds a captured or charged payment. | -3. **Resolve collector** (`authorize`, `authorizeAndCapture` only): `EIP3009_TOKEN_COLLECTOR_ADDRESS` or `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (per `assetTransferMethod`). -4. **Encode `collectorData`** (`authorize`, `authorizeAndCapture` only): raw ERC-3009 signature, or ABI-encoded Permit2 signature. +3. **Resolve collector** (`authorize`, `charge` only): `EIP3009_TOKEN_COLLECTOR_ADDRESS` or `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (per `assetTransferMethod`). +4. **Encode `collectorData`** (`authorize`, `charge` only): raw ERC-3009 signature, or ABI-encoded Permit2 signature. 5. **Call escrow** with the operation-specific fields from the payload. 6. **Wait for receipt**: 60s timeout. 7. **Return result**: tx hash, network, payer, and settled or refunded amount where applicable. From cc131265d847151f06fa8d729c4d5f45a3cc1916 Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Wed, 20 May 2026 09:41:32 -0700 Subject: [PATCH 05/10] docs: keep authCapture generic spec network agnostic Move the EVM-specific serverAuthorization wording into the EVM binding and keep the base authCapture spec network-neutral. Co-authored-by: Cursor --- specs/schemes/authCapture/scheme_authCapture.md | 14 ++++++-------- .../schemes/authCapture/scheme_authCapture_evm.md | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index 3b23612c79..63791ed867 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -18,10 +18,10 @@ Unlike `exact`, which has no built-in mechanism for returning funds, `authCaptur The scheme supports two settlement paths, selected by the operation `type` passed to the facilitator: -| `type` | Behavior | -| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------- | -| `authorize` | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | -| `charge` | Single-shot. Funds sent directly to receiver. CaptureAuthorizer can refund post-settlement. | +| `type` | Behavior | +| :---------- | :--------------------------------------------------------------------------------------------------------------------------- | +| `authorize` | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | +| `charge` | Single-shot. Funds sent directly to receiver. CaptureAuthorizer can refund post-settlement. | ### Two-phase (`type: "authorize"`) @@ -51,9 +51,7 @@ No capture, void, or reclaim — funds are never held in escrow. Facilitators MUST provide a mechanism for servers to perform `authorize`, `charge`, `capture`, `void`, and `refund` operations. Servers select the operation by passing a `type` field to the facilitator's `verify` and `settle` endpoints. Network bindings define the payload fields required for each operation. -Servers MAY self-facilitate by interacting with the escrow contract or network-specific settlement mechanism directly instead of using a third-party facilitator. - -Facilitators MAY require proof that the server controls the signed authorization's `payTo` address before performing server-initiated operations. Facilitators that require this proof MUST signal it with `extra.serverAuthorizationRequired` in the payment requirements. Network bindings may define a `serverAuthorization` field for this purpose. For EVM, `serverAuthorization` is an identity proof over the payment's derived nonce signed by `payTo`. +Facilitators MAY require proof that the server controls the signed authorization's `payTo` address before performing server-initiated operations. Facilitators that require this proof MUST signal it with `extra.serverAuthorizationRequired` in the payment requirements. Network bindings may define a `serverAuthorization` field for this purpose. ## Core Properties @@ -85,7 +83,7 @@ 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). +Network-specific implementation details include contracts, signature formats, and verification logic in per-network documents. ### References diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 3b9e9cd77d..587a0de2b6 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -176,7 +176,7 @@ Freshness is enforced by `salt`: each signing call generates a fresh `bytes32` s ### Server Authorization -A facilitator MAY require `payload.serverAuthorization` before accepting a server-initiated operation. Facilitators that require it MUST set `extra.serverAuthorizationRequired: true` in the payment requirements. `serverAuthorization` is an identity proof that the server controls `requirements.payTo` / `paymentInfo.receiver`; it does not authorize a specific operation or amount. +On EVM, `serverAuthorization` is an identity proof over the payment's derived nonce signed by `requirements.payTo` / `paymentInfo.receiver`. A facilitator MAY require `payload.serverAuthorization` before accepting a server-initiated operation. Facilitators that require it MUST set `extra.serverAuthorizationRequired: true` in the payment requirements. `serverAuthorization` does not authorize a specific operation or amount. ```json { From 39838ce45df6df1a2f233f04f64391fbb116e061 Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Wed, 20 May 2026 09:53:57 -0700 Subject: [PATCH 06/10] docs: add authCapture smart contract operators Document facilitator routing through captureAuthorizer contracts that expose the escrow operation interface. Co-authored-by: Cursor --- specs/schemes/authCapture/scheme_authCapture_evm.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 587a0de2b6..00d64fde0c 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -246,6 +246,12 @@ For smart wallet clients, the signature may be EIP-6492 wrapped (containing depl Servers that self-facilitate perform the same escrow calls directly instead of relaying the operation through a third-party facilitator. In that mode, the server is responsible for the same verification rules, payment state checks, and transaction submission behavior described above. +### Smart Contract Operators + +Facilitators MAY detect when `extra.captureAuthorizer` / `paymentInfo.operator` is a smart contract that exposes the same operation interface as `AuthCaptureEscrow`. In that case, the facilitator MAY forward the mapped operation call to the operator contract instead of calling `AUTH_CAPTURE_ESCROW_ADDRESS` directly. + +The operator contract is then responsible for calling the underlying escrow contract with `msg.sender` equal to the committed `captureAuthorizer`. Facilitators that use this path MUST still apply the same payload verification, payment state checks, collector selection, and receipt handling described above. + ## Error Codes The authCapture scheme uses the standard x402 error codes plus these scheme-specific codes: From 71a0511de0f875900dcdfbc1025463867e391dc7 Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Wed, 20 May 2026 09:57:25 -0700 Subject: [PATCH 07/10] docs: clarify authCapture captureAuthorizer source State that captureAuthorizer is provided by the server in the generic authCapture spec. Co-authored-by: Cursor --- specs/schemes/authCapture/scheme_authCapture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index 63791ed867..d0e30e430e 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -4,7 +4,7 @@ `authCapture` is a payment scheme where funds can be held and settled later. The client authorizes a maximum amount, and the server or 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. +The **captureAuthorizer** is provided by the server and 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. From 01c05286292b4b73c1b07afc87dc2b14cbab9f5a Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Wed, 20 May 2026 10:02:14 -0700 Subject: [PATCH 08/10] docs: add smart operator safety guidance Document gas and wallet safety expectations for facilitators calling smart contract operators. Co-authored-by: Cursor --- specs/schemes/authCapture/scheme_authCapture_evm.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 00d64fde0c..1818b494c4 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -252,6 +252,8 @@ Facilitators MAY detect when `extra.captureAuthorizer` / `paymentInfo.operator` The operator contract is then responsible for calling the underlying escrow contract with `msg.sender` equal to the committed `captureAuthorizer`. Facilitators that use this path MUST still apply the same payload verification, payment state checks, collector selection, and receipt handling described above. +Facilitators MAY support smart contract operators, but SHOULD treat them as untrusted. Facilitators SHOULD NOT send native value. Facilitators SHOULD use a gas-only hot wallet with no token balances or approvals. Facilitators SHOULD cap gas and simulate the exact call before broadcast. + ## Error Codes The authCapture scheme uses the standard x402 error codes plus these scheme-specific codes: From 5cfd10b4e3dda77a93ee1fd82a72012b11b53cb1 Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Wed, 20 May 2026 10:03:52 -0700 Subject: [PATCH 09/10] docs: rename auth capture scheme literal Use auth-capture as the scheme name in the auth capture specs and examples. Co-authored-by: Cursor --- .../schemes/authCapture/scheme_authCapture.md | 8 +++---- .../authCapture/scheme_authCapture_evm.md | 24 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/authCapture/scheme_authCapture.md index d0e30e430e..0ea9458381 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/authCapture/scheme_authCapture.md @@ -1,12 +1,12 @@ -# Scheme: `authCapture` +# Scheme: `auth-capture` ## Summary -`authCapture` is a payment scheme where funds can be held and settled later. The client authorizes a maximum amount, and the server or 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). +`auth-capture` is a payment scheme where funds can be held and settled later. The client authorizes a maximum amount, and the server or 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 provided by the server and 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. +Unlike `exact`, which has no built-in mechanism for returning funds, `auth-capture` supports returning funds to the client through void, refund, and reclaim. ## Example Use Cases @@ -75,7 +75,7 @@ Two absolute-timestamp deadlines govern the payment lifecycle (network-specific ## Relationship to `exact` -| Aspect | `exact` | `authCapture` | +| Aspect | `exact` | `auth-capture` | | :--------- | :----------------- | :-------------------------------------------------------------------- | | Settlement | Immediate transfer | Via escrow (two-phase) or direct with refund capability (single-shot) | | Refundable | No | Yes (both paths) | diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/authCapture/scheme_authCapture_evm.md index 1818b494c4..a51fedf8b8 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/authCapture/scheme_authCapture_evm.md @@ -1,8 +1,8 @@ -# Scheme: `authCapture` on `EVM` +# Scheme: `auth-capture` on `EVM` ## Summary -The `authCapture` scheme on EVM uses the [base/commerce-payments](https://github.com/base/commerce-payments) contract stack: +The `auth-capture` 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 (same address on every supported chain). - **Token Collectors**: Universal canonical addresses, one per `assetTransferMethod`: @@ -14,14 +14,14 @@ The client signs a single signature (ERC-3009 or Permit2). The server chooses th ## PaymentRequirements -AuthCapture-accepting servers advertise with scheme `authCapture`: +Servers accepting auth-capture payments advertise with scheme `auth-capture`: ```json { "x402Version": 2, "accepts": [ { - "scheme": "authCapture", + "scheme": "auth-capture", "network": "eip155:8453", "amount": "1000000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", @@ -91,7 +91,7 @@ The server-to-facilitator payload carries a `type` discriminator. For `authorize { "x402Version": 2, "resource": { "url": "https://api.example.com/resource", "method": "GET" }, - "accepted": { "scheme": "authCapture", "...": "..." }, + "accepted": { "scheme": "auth-capture", "...": "..." }, "payload": { "type": "authorize", "authorization": { @@ -128,7 +128,7 @@ The server-to-facilitator payload carries a `type` discriminator. For `authorize { "x402Version": 2, "resource": { "url": "https://api.example.com/resource", "method": "GET" }, - "accepted": { "scheme": "authCapture", "...": "..." }, + "accepted": { "scheme": "auth-capture", "...": "..." }, "payload": { "type": "authorize", "permit2Authorization": { @@ -205,7 +205,7 @@ Facilitators verify EOA `payTo` addresses with ECDSA recovery and contract `payT The facilitator performs these checks in order: 1. **Type guard**: Verify `payload.type` is one of `authorize`, `charge`, `capture`, `void`, or `refund`, and that the payload includes the fields required for that operation. -2. **Scheme match**: `requirements.scheme === "authCapture"` and `payload.accepted.scheme === "authCapture"`. +2. **Scheme match**: `requirements.scheme === "auth-capture"` and `payload.accepted.scheme === "auth-capture"`. 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. **PaymentInfo match**: Verify `paymentInfo.operator === extra.captureAuthorizer`, `paymentInfo.receiver === requirements.payTo`, `paymentInfo.token === requirements.asset`, `paymentInfo.maxAmount === requirements.amount`, deadlines and fee fields match `extra`, and `paymentInfo.preApprovalExpiry` is derived from `maxTimeoutSeconds`. @@ -256,18 +256,18 @@ Facilitators MAY support smart contract operators, but SHOULD treat them as untr ## Error Codes -The authCapture scheme uses the standard x402 error codes plus these scheme-specific codes: +The auth-capture scheme uses the standard x402 error codes plus these scheme-specific codes: ### Verification Errors | Error Code | Description | | :---------------------------------- | :-------------------------------------------------------------------------------- | | `invalid_payload_format` | Payload doesn't match the fields required for its `type`. | -| `invalid_operation_type` | `payload.type` is not one of the supported authCapture operations. | -| `unsupported_scheme` | Scheme is not `authCapture`. | +| `invalid_operation_type` | `payload.type` is not one of the supported auth-capture operations. | +| `unsupported_scheme` | Scheme is not `auth-capture`. | | `network_mismatch` | Payload network doesn't match requirements. | | `invalid_network` | Network format is not `eip155:`. | -| `invalid_authCapture_extra` | Extra is missing required fields. | +| `invalid_auth_capture_extra` | Extra is missing required fields. | | `payment_info_mismatch` | `paymentInfo` does not match the accepted payment requirements. | | `missing_server_authorization` | Facilitator requires `serverAuthorization`, but the payload omitted it. | | `invalid_server_authorization` | `serverAuthorization` does not verify against `requirements.payTo`. | @@ -277,7 +277,7 @@ The authCapture scheme uses the standard x402 error codes plus these scheme-spec | `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. | +| `invalid_auth_capture_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`. | From aed28112f59fc78fca35b77b32367a9df215b902 Mon Sep 17 00:00:00 2001 From: Andrew Reder Date: Wed, 20 May 2026 10:22:37 -0700 Subject: [PATCH 10/10] rename files/folders --- .../scheme_authCapture.md => auth-capture/scheme_auth_capture.md} | 0 .../scheme_auth_capture_evm.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename specs/schemes/{authCapture/scheme_authCapture.md => auth-capture/scheme_auth_capture.md} (100%) rename specs/schemes/{authCapture/scheme_authCapture_evm.md => auth-capture/scheme_auth_capture_evm.md} (100%) diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/auth-capture/scheme_auth_capture.md similarity index 100% rename from specs/schemes/authCapture/scheme_authCapture.md rename to specs/schemes/auth-capture/scheme_auth_capture.md diff --git a/specs/schemes/authCapture/scheme_authCapture_evm.md b/specs/schemes/auth-capture/scheme_auth_capture_evm.md similarity index 100% rename from specs/schemes/authCapture/scheme_authCapture_evm.md rename to specs/schemes/auth-capture/scheme_auth_capture_evm.md