Skip to content
Merged
15 changes: 12 additions & 3 deletions packages/evm/src/escrow/facilitator/scheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,20 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
}
}

// Verify amount meets requirements
if (BigInt(escrowPayload.authorization.value) < BigInt(requirements.amount)) {
// Verify amount exactly matches requirements
if (BigInt(escrowPayload.authorization.value) !== BigInt(requirements.amount)) {
return {
isValid: false,
invalidReason: 'insufficient_amount',
invalidReason: 'amount_mismatch',
payer,
}
}

// Verify authorization recipient is the token collector
if (escrowPayload.authorization.to.toLowerCase() !== extra.tokenCollector.toLowerCase()) {
return {
isValid: false,
invalidReason: 'token_collector_mismatch',
payer,
}
}
Expand Down
93 changes: 35 additions & 58 deletions specs/schemes/escrow/scheme_escrow.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,102 +2,79 @@

## Summary

The `escrow` scheme transfers funds through an on-chain escrow contract, decoupling authorization from settlement. The client signs once to authorize a maximum amount, and the facilitator settles through the [Commerce Payments Protocol](https://github.com/base/commerce-payments) — routing funds into escrow (pre-settlement hold) or directly to the receiver (post-settlement refundable).
`escrow` is a scheme that routes funds through escrow, decoupling authorization from final settlement. The client authorizes a maximum amount, and the facilitator settles — either holding funds in escrow or sending them directly to the receiver with post-settlement refund capability.

This scheme reuses audited commerce-payments contracts deployed on Base and other EVM chains.
Unlike `exact`, which transfers funds immediately and irrevocably, `escrow` supports refundable payments across both settlement paths.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me, but make sure wording is what you expecting. The md file might have unnecessary information

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — the escrow abstract spec is longer than exact's, but escrow has more moving parts (two settlement paths, expiry tiers, fee system, post-settlement actions). I think the current content is justified. Happy to trim if you spot anything specific that feels redundant though.

## Example Use Cases

- Refundable payments with buyer protection
- Post-settlement refunds via the charge path
- Subscription / session billing with periodic captures
- Delayed delivery where the client needs recourse if the service is unsatisfactory
- Subscription or session billing with periodic captures against a single authorization

## Settlement Methods

The scheme supports two settlement paths through the commerce-payments operator:
The scheme supports two settlement paths:

| Method | Function | Behavior |
| :---------- | :------------ | :----------------------------------------------------------- |
| `authorize` | `authorize()` | Funds held in escrow. Can be captured, refunded, or voided. |
| `charge` | `charge()` | Funds sent directly to receiver. Refundable post-settlement. |

Both methods share identical function signatures and use the same operator, fee system, and token collector infrastructure.

## Lifecycle
| Method | Behavior |
| :---------- | :------------------------------------------------------------------------- |
| `authorize` | Funds held in escrow. Can be captured, refunded, voided, or reclaimed. |
| `charge` | Funds sent directly to receiver. Refundable post-settlement by the operator. |

### Authorize (default)

```
SIGN → AUTHORIZE → RESOURCE DELIVERED
AUTHORIZE → RESOURCE DELIVERED → CAPTURE / REFUND / VOID
```

1. **Sign**: Client signs an ERC-3009 `receiveWithAuthorization` for the maximum amount
2. **Authorize**: Facilitator calls `authorize()` on the operator — funds locked in escrow
3. **Resource delivered**: Server returns the resource (HTTP 200)

Post-settlement, the commerce-payments contracts enable capture, refund, void, or reclaim — see [Commerce Payments Protocol](#commerce-payments-protocol).
1. **Authorize**: Client authorization is submitted — funds locked in escrow
2. **Resource delivered**: Server returns the resource (HTTP 200)
3. **Post-settlement**: Operator can capture, refund, or void. Client can reclaim after the capture deadline if the operator disappears.

### Charge

```
SIGN → CHARGE → RESOURCE DELIVERED
CHARGE → RESOURCE DELIVERED → (REFUND)
```

1. **Sign**: Client signs an ERC-3009 authorization (same as above)
2. **Charge**: Facilitator calls `charge()` on the operator — funds go directly to receiver
3. **Resource delivered**: Server returns the resource (HTTP 200)

Post-settlement, the operator can refund within `refundExpiry` if needed. Unlike the authorize path, the payer cannot `reclaim()` — funds are already with the receiver.

## Relationship to `exact`
1. **Charge**: Client authorization is submitted — funds go directly to receiver
2. **Resource delivered**: Server returns the resource (HTTP 200)
3. **Post-settlement**: Operator can issue a refund within the refund window if the client is dissatisfied. Unlike authorize, the client cannot reclaim — funds are already with the receiver, so refunds require operator action.

| Aspect | `exact` | `escrow` |
| :----------------- | :----------------- | :------------------------------------------------- |
| Settlement | Immediate transfer | Via escrow contract (authorize) or direct (charge) |
| Refundable | No | Yes (both paths) |
| Fee system | None | Commerce-payments managed (min/max bps) |
| Gas payer | Facilitator | Facilitator |
| Signature | ERC-3009 / Permit2 | ERC-3009 |
| On-chain contracts | Token only | Token + Escrow + Operator + Collector |

The `charge` settlement method gives `escrow` a direct-settlement path (like `exact`) while retaining post-settlement refund capability through the commerce-payments infrastructure.

## Security Considerations
## Core Properties

### Fund Safety

- Funds held in audited [AuthCaptureEscrow](https://github.com/base/commerce-payments) contract
- Cannot overcharge — `amount` capped by client-signed `maxAmount`
- Client can reclaim funds after `authorizationExpiry` if operator disappears
- Fee bounds (`minFeeBps`/`maxFeeBps`) are client-signed and enforced on-chain
- Cannot overcharge — settlement amount is capped by the client-signed maximum
- Client can reclaim escrowed funds after the capture deadline if the operator disappears (authorize path)
- Fee bounds are client-signed and enforced at settlement

### Replay Prevention

- Nonces derived from `keccak256(chainId, escrowAddress, paymentInfoHash)` — unique per payment
- ERC-3009 nonce consumed on-chain by the token contract
- `salt` field provides additional entropy for session uniqueness
- Each payment has a unique nonce derived from the payment parameters
- Nonce is consumed on-chain at settlement, preventing double-spend

### Expiry Enforcement

The contract enforces strict ordering: `preApprovalExpiry <= authorizationExpiry <= refundExpiry`
Three ordered deadlines govern the payment lifecycle:

- `preApprovalExpiry`: Deadline for the ERC-3009 signature (doubles as `validBefore`)
- `authorizationExpiry`: Deadline for capturing escrowed funds
- `refundExpiry`: Deadline for requesting refunds on captured payments
- **Authorization deadline**: Last moment to submit the client's authorization for settlement
- **Capture deadline**: Last moment to capture escrowed funds (authorize path); after this, the client can reclaim
- **Refund deadline**: Last moment to issue a refund on captured or charged payments

## Appendix
## Relationship to `exact`

### Commerce Payments Protocol
| Aspect | `exact` | `escrow` |
| :--------- | :----------------- | :---------------------------------------------- |
| Settlement | Immediate transfer | Via escrow (authorize) or direct with refund capability (charge) |
| Refundable | No | Yes (both paths) |
| Fee system | None | Configurable (min/max bounds, client-signed) |

The escrow scheme is built on Base's [Commerce Payments Protocol](https://blog.base.dev/commerce-payments-protocol), which provides:
## Appendix

- **Escrow**: Singleton contract managing fund locking, capture, refund, and reclaim
- **Operators**: Route payments through escrow with configurable fees
- **Token Collectors**: Pluggable modules for different token authorization methods (ERC-3009, Permit2)
Network-specific implementation details (contracts, signature formats, verification logic) are in per-network documents: `scheme_escrow_evm.md` (EVM).

### References

- [Commerce Payments Protocol](https://github.com/base/commerce-payments)
- [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009)
- [Escrow Scheme Proposal — Agentokratia (Issue #834)](https://github.com/coinbase/x402/issues/834)
- [Escrow Scheme Proposal — x402r (Issue #1011)](https://github.com/coinbase/x402/issues/1011)
9 changes: 5 additions & 4 deletions specs/schemes/escrow/scheme_escrow_evm.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ The facilitator performs these checks in order:
4. **Extra validation**: Verify `requirements.extra` contains required escrow fields (`escrowAddress`, `operatorAddress`, `tokenCollector`)
5. **Time window**: Verify `validBefore > now + 6s` (not expired) and `validAfter <= now` (active)
6. **ERC-3009 signature**: Recover signer from EIP-712 typed data (`ReceiveWithAuthorization` primary type) and verify matches `authorization.from`
7. **Amount**: Verify `authorization.value >= requirements.amount`
8. **Token match**: Verify `paymentInfo.token === requirements.asset`
9. **Receiver match**: Verify `paymentInfo.receiver === requirements.payTo`
10. **Balance check**: Verify payer has sufficient token balance (soft check — skip on RPC failure)
7. **Amount**: Verify `authorization.value === requirements.amount`
8. **Recipient match**: Verify `authorization.to === requirements.extra.tokenCollector`
9. **Token match**: Verify `paymentInfo.token === requirements.asset`
10. **Receiver match**: Verify `paymentInfo.receiver === requirements.payTo`
11. **Balance check**: Verify payer has sufficient token balance (soft check — skip on RPC failure)

### EIP-6492 Support

Expand Down