Skip to content

Commit e6263f5

Browse files
A1igatorclaude
andcommitted
Address x402 PR #1425 review feedback
- Enforce strict amount equality (=== not >=) in verification - Add authorization.to === tokenCollector check - Make scheme_escrow.md network-agnostic (remove EVM-specific references) - Clarify charge path refund mechanics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent db70319 commit e6263f5

3 files changed

Lines changed: 52 additions & 65 deletions

File tree

packages/evm/src/escrow/facilitator/scheme.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,20 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
142142
}
143143
}
144144

145-
// Verify amount meets requirements
146-
if (BigInt(escrowPayload.authorization.value) < BigInt(requirements.amount)) {
145+
// Verify amount exactly matches requirements
146+
if (BigInt(escrowPayload.authorization.value) !== BigInt(requirements.amount)) {
147147
return {
148148
isValid: false,
149-
invalidReason: 'insufficient_amount',
149+
invalidReason: 'amount_mismatch',
150+
payer,
151+
}
152+
}
153+
154+
// Verify authorization recipient is the token collector
155+
if (escrowPayload.authorization.to.toLowerCase() !== extra.tokenCollector.toLowerCase()) {
156+
return {
157+
isValid: false,
158+
invalidReason: 'token_collector_mismatch',
150159
payer,
151160
}
152161
}

specs/schemes/escrow/scheme_escrow.md

Lines changed: 35 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,102 +2,79 @@
22

33
## Summary
44

5-
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).
5+
`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.
66

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

99
## Example Use Cases
1010

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

1515
## Settlement Methods
1616

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

19-
| Method | Function | Behavior |
20-
| :---------- | :------------ | :----------------------------------------------------------- |
21-
| `authorize` | `authorize()` | Funds held in escrow. Can be captured, refunded, or voided. |
22-
| `charge` | `charge()` | Funds sent directly to receiver. Refundable post-settlement. |
23-
24-
Both methods share identical function signatures and use the same operator, fee system, and token collector infrastructure.
25-
26-
## Lifecycle
19+
| Method | Behavior |
20+
| :---------- | :------------------------------------------------------------------------- |
21+
| `authorize` | Funds held in escrow. Can be captured, refunded, voided, or reclaimed. |
22+
| `charge` | Funds sent directly to receiver. Refundable post-settlement by the operator. |
2723

2824
### Authorize (default)
2925

3026
```
31-
SIGN → AUTHORIZE → RESOURCE DELIVERED
27+
AUTHORIZE → RESOURCE DELIVERED → CAPTURE / REFUND / VOID
3228
```
3329

34-
1. **Sign**: Client signs an ERC-3009 `receiveWithAuthorization` for the maximum amount
35-
2. **Authorize**: Facilitator calls `authorize()` on the operator — funds locked in escrow
36-
3. **Resource delivered**: Server returns the resource (HTTP 200)
37-
38-
Post-settlement, the commerce-payments contracts enable capture, refund, void, or reclaim — see [Commerce Payments Protocol](#commerce-payments-protocol).
30+
1. **Authorize**: Client authorization is submitted — funds locked in escrow
31+
2. **Resource delivered**: Server returns the resource (HTTP 200)
32+
3. **Post-settlement**: Operator can capture, refund, or void. Client can reclaim after the capture deadline if the operator disappears.
3933

4034
### Charge
4135

4236
```
43-
SIGN → CHARGE → RESOURCE DELIVERED
37+
CHARGE → RESOURCE DELIVERED → (REFUND)
4438
```
4539

46-
1. **Sign**: Client signs an ERC-3009 authorization (same as above)
47-
2. **Charge**: Facilitator calls `charge()` on the operator — funds go directly to receiver
48-
3. **Resource delivered**: Server returns the resource (HTTP 200)
49-
50-
Post-settlement, the operator can refund within `refundExpiry` if needed. Unlike the authorize path, the payer cannot `reclaim()` — funds are already with the receiver.
51-
52-
## Relationship to `exact`
40+
1. **Charge**: Client authorization is submitted — funds go directly to receiver
41+
2. **Resource delivered**: Server returns the resource (HTTP 200)
42+
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.
5343

54-
| Aspect | `exact` | `escrow` |
55-
| :----------------- | :----------------- | :------------------------------------------------- |
56-
| Settlement | Immediate transfer | Via escrow contract (authorize) or direct (charge) |
57-
| Refundable | No | Yes (both paths) |
58-
| Fee system | None | Commerce-payments managed (min/max bps) |
59-
| Gas payer | Facilitator | Facilitator |
60-
| Signature | ERC-3009 / Permit2 | ERC-3009 |
61-
| On-chain contracts | Token only | Token + Escrow + Operator + Collector |
62-
63-
The `charge` settlement method gives `escrow` a direct-settlement path (like `exact`) while retaining post-settlement refund capability through the commerce-payments infrastructure.
64-
65-
## Security Considerations
44+
## Core Properties
6645

6746
### Fund Safety
6847

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

7452
### Replay Prevention
7553

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

8057
### Expiry Enforcement
8158

82-
The contract enforces strict ordering: `preApprovalExpiry <= authorizationExpiry <= refundExpiry`
59+
Three ordered deadlines govern the payment lifecycle:
8360

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

88-
## Appendix
65+
## Relationship to `exact`
8966

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

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

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

9877
### References
9978

100-
- [Commerce Payments Protocol](https://github.com/base/commerce-payments)
101-
- [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009)
10279
- [Escrow Scheme Proposal — Agentokratia (Issue #834)](https://github.com/coinbase/x402/issues/834)
10380
- [Escrow Scheme Proposal — x402r (Issue #1011)](https://github.com/coinbase/x402/issues/1011)

specs/schemes/escrow/scheme_escrow_evm.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,11 @@ The facilitator performs these checks in order:
134134
4. **Extra validation**: Verify `requirements.extra` contains required escrow fields (`escrowAddress`, `operatorAddress`, `tokenCollector`)
135135
5. **Time window**: Verify `validBefore > now + 6s` (not expired) and `validAfter <= now` (active)
136136
6. **ERC-3009 signature**: Recover signer from EIP-712 typed data (`ReceiveWithAuthorization` primary type) and verify matches `authorization.from`
137-
7. **Amount**: Verify `authorization.value >= requirements.amount`
138-
8. **Token match**: Verify `paymentInfo.token === requirements.asset`
139-
9. **Receiver match**: Verify `paymentInfo.receiver === requirements.payTo`
140-
10. **Balance check**: Verify payer has sufficient token balance (soft check — skip on RPC failure)
137+
7. **Amount**: Verify `authorization.value === requirements.amount`
138+
8. **Recipient match**: Verify `authorization.to === requirements.extra.tokenCollector`
139+
9. **Token match**: Verify `paymentInfo.token === requirements.asset`
140+
10. **Receiver match**: Verify `paymentInfo.receiver === requirements.payTo`
141+
11. **Balance check**: Verify payer has sufficient token balance (soft check — skip on RPC failure)
141142

142143
### EIP-6492 Support
143144

0 commit comments

Comments
 (0)