diff --git a/specs/schemes/authCapture/scheme_authCapture.md b/specs/schemes/auth-capture/scheme_auth_capture.md similarity index 57% rename from specs/schemes/authCapture/scheme_authCapture.md rename to specs/schemes/auth-capture/scheme_auth_capture.md index 836a4cc947..0ea9458381 100644 --- a/specs/schemes/authCapture/scheme_authCapture.md +++ b/specs/schemes/auth-capture/scheme_auth_capture.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 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 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. +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 @@ -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 | +| :---------- | :--------------------------------------------------------------------------------------------------------------------------- | +| `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 (`autoCapture: false`, default) +### Two-phase (`type: "authorize"`) ``` 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: "charge"`) ``` CHARGE → RESOURCE DELIVERED → (REFUND) @@ -47,6 +47,12 @@ 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 `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. + +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 ### Fund Safety @@ -69,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) | @@ -77,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/auth-capture/scheme_auth_capture_evm.md similarity index 61% rename from specs/schemes/authCapture/scheme_authCapture_evm.md rename to specs/schemes/auth-capture/scheme_auth_capture_evm.md index 888e086ba8..a51fedf8b8 100644 --- a/specs/schemes/authCapture/scheme_authCapture_evm.md +++ b/specs/schemes/auth-capture/scheme_auth_capture_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`: @@ -10,18 +10,18 @@ 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 `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 -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", @@ -36,7 +36,7 @@ AuthCapture-accepting servers advertise with scheme `authCapture`: "minFeeBps": 0, "maxFeeBps": 1000, "feeRecipient": "0xFeeRecipientAddress", - "autoCapture": false, + "serverAuthorizationRequired": true, "assetTransferMethod": "eip3009" } } @@ -56,7 +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). | -| `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | +| `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 @@ -73,7 +73,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 `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()` | +| `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. `charge` is the single-shot path: funds are sent directly to the receiver with refund capability. ### EIP-3009 (default) @@ -81,8 +91,9 @@ The payload carries the signature and the client-generated `salt`. The facilitat { "x402Version": 2, "resource": { "url": "https://api.example.com/resource", "method": "GET" }, - "accepted": { "scheme": "authCapture", "...": "..." }, + "accepted": { "scheme": "auth-capture", "...": "..." }, "payload": { + "type": "authorize", "authorization": { "from": "0xPayerAddress", "to": "0xEIP3009TokenCollectorAddress", @@ -92,7 +103,8 @@ The payload carries the signature and the client-generated `salt`. The facilitat "nonce": "0xf374...3480" }, "signature": "0x2d6a...571c", - "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc", + "paymentInfo": { "...": "..." } } } ``` @@ -116,8 +128,9 @@ The payload carries the signature and the client-generated `salt`. The facilitat { "x402Version": 2, "resource": { "url": "https://api.example.com/resource", "method": "GET" }, - "accepted": { "scheme": "authCapture", "...": "..." }, + "accepted": { "scheme": "auth-capture", "...": "..." }, "payload": { + "type": "authorize", "permit2Authorization": { "from": "0xPayerAddress", "permitted": { @@ -129,7 +142,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 +174,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 + +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 +{ + "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 `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 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"`. +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 === "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. **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 `extra.serverAuthorizationRequired` is `true`, verify `payload.serverAuthorization` as an EIP-712 identity proof from `requirements.payTo` / `paymentInfo.receiver`. +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. ### EIP-6492 Support @@ -185,37 +228,61 @@ 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 | +| :-------------------- | :------------------------------- | :-------------------------------------------------------------------- | +| `authorize` | `AuthCaptureEscrow.authorize()` | Collects tokens into escrow for later capture or void. | +| `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`, `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. +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. + +### 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. + +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: +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 `Eip3009Payload` or `Permit2Payload`. | -| `unsupported_scheme` | Scheme is not `authCapture`. | +| `invalid_payload_format` | Payload doesn't match the fields required for its `type`. | +| `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`. | | `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. | +| `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`. | | `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. |