feat: x402BatchSettlement contract#1950
feat: x402BatchSettlement contract#1950CarsonRoscoe wants to merge 11 commits intox402-foundation:mainfrom
Conversation
ilikesymmetry
left a comment
There was a problem hiding this comment.
First pass gut says lots of code and would like to trim down and simplify where possible. Going to take more passes.
|
Directionally, I think we should move towards more statelessness like this: https://gist.github.com/ilikesymmetry/b351bd6ca47a9419c91b195bce332952 |
| "payTo": "0xServerReceiverAddress", | ||
| "maxTimeoutSeconds": 3600, | ||
| "extra": { | ||
| "receiver": "0xServerReceiverAddress", |
There was a problem hiding this comment.
receiver is redundant, should use payTo directly
| /// @notice Finalize withdrawal after delay has elapsed. | ||
| /// Only the payer or receiverAuthorizer can call. | ||
| function finalizeWithdraw(ChannelConfig calldata config) external nonReentrant { | ||
| if (msg.sender != config.payer && msg.sender != config.receiverAuthorizer) { |
There was a problem hiding this comment.
Do we need to gate finalizeWithdraw? Should be fair game to be called by everyone once the grace period has passed. Then we could also get rid of finalizeWithdrawWithSignature including typehash
| | `"initiateWithdraw"` | Client requests withdrawal | Record withdrawal timestamp on channel | | ||
| | `"finalizeWithdraw"` | After delay elapses | Refund unclaimed deposit, reduce balance | |
There was a problem hiding this comment.
Remove initiateWithdraw/finalizeWithdraw settleActions, these server as escape hatch only to be called by client directly
Description
Implements
x402BatchSettlement, the onchain escrow contract powering thebatch-settlementx402 payment scheme. This scheme is designed for high-frequency API access, clients pre-fund a subchannel, sign off-chain cumulative vouchers per request, and the server batch-claims them onchain at its discretion. No per-request gas cost.Deployed to Base Sepolia (
0x40200e6f073aCB938e0Cf766B83f4E5286E60003) for parallel SDK development.Contract Overview
The contract manages two layers: a service registry (one record per server) and subchannels (one per
(serviceId, payer, token)triple).Service lifecycle
Servers register a
serviceIdfirst-come-first-serve, specifying an initialpayToaddress, an initial authorizer, and awithdrawWindow(bounded 15 min – 30 days). Registration can be done directly or gaslessly via an EIP-712 signedregisterFor. Admin operations (add/remove authorizer, updatepayTo, update withdraw window) are all gaslessly submittable — signed by an existing authorizer and consumed with anadminNonceto prevent replay. At least one authorizer must always remain.Subchannel lifecycle
A subchannel is identified by
(serviceId, payer, token), making services token-agnostic — any ERC-20 can be deposited. Three gasless deposit methods are supported:receiveWithAuthorization) — ideal for USDC, fully off-chainpermitWitnessTransferFrom) — works for any ERC-20 with Permit2 approval, with aDepositWitnessbinding the deposit to a specific servicePayment flow
Clients sign EIP-712
Vouchermessages per request. Each voucher carries a monotonically increasingnonceand a cumulativecumulativeAmount. The server accumulates these off-chain and batches them into a singleclaim(serviceId, token, VoucherClaim[])call. Claimed amounts accumulate inunsettled[serviceId][token]and are transferred topayTovia a separatesettle(serviceId, token). The split lets servers amortize gas across arbitrarily many payers.Voucher signature verification
Signatures are verified by pure ECDSA recovery — no EIP-1271. Smart contract wallets are supported via client signer delegation: a payer authorizes an EOA hot wallet to sign on their behalf (
authorizeClientSigner/authorizeClientSignerFor). Delegation is per-service and uses aclientNoncefor gasless replay protection.Withdrawals
Three exit paths:
CooperativeWithdrawas authorizer; unclaimed deposit is refunded immediately, can batch multiple payers.RequestWithdrawal(includeswithdrawNonceto prevent replay after cooperative withdraw); facilitator submitsrequestWithdrawalFor. After the window, anyone callswithdraw.requestWithdrawaldirectly.Both reset paths preserve the voucher
nonceso old vouchers cannot be replayed after re-deposit. ThewithdrawNonceincrements on each cooperative withdraw to prevent authorizer signature replay.Tests
Test Results — 174/174 passed, 0 failed
X402BatchSettlementTestCoverage — Production Contracts
x402BatchSettlement.solx402BatchSettlementis the primary new contract and hits 100% on all four coverage axesChecklist