Proof of concept integrating Kleros V2 dispute resolution with x402 auth-capture payments. Kleros is the operator of the escrow: the buyer's money is locked until the merchant is paid, the payer is refunded, or Kleros rules on who gets what.
The escrow is Coinbase's commerce-payments AuthCaptureEscrow v1.1, consumed untouched and pinned as a git submodule at 6b0a5ef. x402's auth-capture scheme lets a merchant name a custom operator for it; KlerosCaptureAuthorizer is that operator.
A payment ends in one of four ways: the dispute window passes and the merchant is paid; the payer releases it early; the merchant refunds it in full; or the payer disputes and Kleros rules. A partial refund only concedes that part; the rest still ends one of those ways. The lifecycle:
- The offer. The merchant server's 402 names
KlerosCaptureAuthorizeras a custom, collect-only operator, with no expiry and no fees. The SDK's merchant helper builds that part of the route. - The payment. The buyer pays through the SDK's fetch wrapper. It keeps the 402 verbatim in an agreement file and signs the payment with that file's keccak256 as
PaymentInfo.salt, binding the payment to exactly the offer it was made against. The x402 facilitator verifies the payment and callsauthorizeon the operator, which locks the funds in the escrow and opens the dispute window. - The window. The payer can release early with
payerAccept. The merchant can refund any amount withmerchantRefund; refunding everything settles the payment. If the window passes silently, anyone callscaptureIfUnchallengedand the merchant is paid. - The dispute. Within the window the payer calls
dispute, paying the arbitration fee. The amount is clamped to what is still disputable; the undisputed remainder goes to the merchant at once, since no ruling can send it anywhere else. The agreement file is pinned and its URI stored with the dispute. - The ruling. Pay the merchant: the disputed amount is captured. Refund the payer: it is voided back. Refuse to arbitrate: the merchant is paid.
executeRulingsettles a ruling the arbitrator could not settle in its own transaction.
- Dispute template and data mappings (template/) are registered on Kleros' template registry by the operator's constructor. The single mapping calls
getCaseData(disputeID)on the operator, which returns everything the case page shows as one struct. - The agreement is linked on the case page next to the agreement hash, the payment's salt. A juror downloads the file and checks
cast keccak < agreement.jsonagainst the hash: a match means the file is the offer the payer paid against, and neither party can claim other terms. - The dispute policy (template/policy.md, pinned at
/ipfs/QmUXtVE2gGzWtYYhM68XTQMcDDfYFXkgTgeeVUXDbQwUnA) tells jurors how to establish the offer from the agreement or, failing that, from the evidence; how to compare the delivery with it; and that the payer bears the burden of proof. A mock policy for the PoC.
- Collect-only operator. The facilitator relays
authorizeand nothing else; capture and refund are the operator's own calls, driven by the window, the parties or the ruling. The 402 therefore carriespaymentFlow: "escrow",captureMode: "deferred"andreceiverAuthorizer: zeroAddress, andchargeis disabled on the operator. - No expiry on the escrow authorization. Capture and refund deadlines are the largest
uint48, so no ruling can arrive too late. The operator refuses deadlines shorter than its dispute window plus an arbitration buffer. - Only the payer can dispute. Silence pays the merchant, so the payer is the only party it can hurt.
- Refuse to arbitrate pays the merchant. Refusing to arbitrate should not change the normal payment flow. Refund by default would let a payer win by making a case the jurors cannot decide.
- No arbitration fee return. The payer pays the fee and is not refunded, even if he wins. Must be improved for a production deployment.
- No fees to the operator. The fee fields exist in the 402 and the contract forwards whatever they say, so a fee can be introduced without a contract change.
- No exit if the Court never rules. The funds stay in escrow until a ruling is executed. A stale-dispute path was considered and left out of the PoC.
- Own buyer client for this scheme. x402's fetch wrapper decodes the 402 before any hook sees its raw bytes, which the agreement needs, and its auth-capture client draws a random salt with no way to set one. The SDK replaces both for this scheme, built on x402's types, header codecs, signer interface and payload format, so what reaches the merchant server and the facilitator is the official payload with the agreement hash as salt. Merchant server and facilitator are x402's own.
- Amounts on the case page are in the token's base units. The Court renders what the mapping returns; formatting by decimals belongs in the Court's template engine.
- The template is immutable. Registered once at construction; changing it means redeploying the operator.
- Own escrow on the test network. Coinbase's canonical escrow is not deployed on Arbitrum Sepolia, so the PoC deploys its own copy there. In production the operator is deployed against the canonical escrow and collector of its network.
All contracts are deployed and verified on Arbitrum Sepolia, interacting with the Kleros v2 testnet and using the Agentic Commerce Court (court 8), 3 jurors per dispute, classic dispute kit, arbitration cost 0.00081 ETH. The operator's dispute window is 3 days and its arbitration buffer 7 days.
| Contract | Address |
|---|---|
| AuthCaptureEscrow v1.1 (our deployment, their code untouched) | 0x3e1e17725899ce96AF5C95b9f95a9c74588cf622 |
| ERC3009PaymentCollector | 0xE96E9f608fE62a8A8A29cf1B9BBAE33799DaF8e3 |
| KlerosCaptureAuthorizer (the operator) | 0x729CeC589286515A9bF28Fcdf0e71D2f94fAf674 |
| Payment token (Circle testnet USDC) | 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d |
| Kleros KlerosCore | 0xE8442307d36e9bf6aB27F1A009F95CE8E11C3479 |
| Kleros DisputeTemplateRegistry | 0xe763d31Cb096B4bc7294012B78FC7F148324ebcb |
To be run. Every settlement path is to run on the real testnet court, against the contracts above, through the official x402 merchant server and facilitator built from x402 main:
- Unchallenged capture: paid, window passes,
captureIfUnchallengedpays the merchant. - Early release: paid,
payerAcceptpays the merchant inside the window. - Full concession: paid,
merchantRefundof the full amount voids it back to the payer. - Full dispute: paid, disputed in full, ruled at the Court, executed.
- Partial dispute: paid, part disputed, remainder captured at dispute time, the rest ruled.
| Case | Scenario | Ruling |
|---|---|---|
| tbd |
The merchant server and the facilitator are x402's own example apps, demo/merchant.ts and demo/facilitator.ts, copied from the pinned x402 submodule with Arbitrum Sepolia as the network. The merchant's route is built with the SDK; the facilitator has the operator on its custom-operator allowlist. The buyer is the SDK's fetch wrapper, run by demo/buy.ts, and the calls while the payment is in escrow are made by demo/settle.ts. Three things about running them for the demo:
- Their auth-capture entry points landed on x402
mainwith PR #3203 (merged 2026-10-06) and are unreleased:@x402/evm2.28.0 ships the client only. demo/build-x402.sh builds@x402/core,@x402/extensions,@x402/evmand@x402/expressfrom the submodule, pinned at7f2b2f1. - They only know Coinbase's canonical escrow addresses, from the table in
src/auth-capture/constants.ts; otherwise the server rewritesauthCaptureEscrowto the canonical address and the facilitator checks the payment against contracts with no code on Arbitrum Sepolia. The build applies demo/x402-arbitrum-sepolia-escrow.patch, which adds our escrow and collector to that table, and reverts it afterwards. In production, against the canonical escrow, nothing is patched. - The facilitator verifies a custom operator by simulating the
authorizecall, so its signer needs an RPC that supportseth_simulateV1. Arbitrum's public endpoint does. Its gas cap for that call is 1,000,000;authorizeuses about 380,000 in the Foundry suite.
Copy demo/.env.example to demo/.env and fill it in: the facilitator's key, the merchant's address and key, the buyer's key, and a Pinata JWT for disputes. The facilitator pays gas for authorize, the buyer pays in USDC plus the arbitration cost in ETH, the merchant pays gas for refunds. Then, from demo/, each in its own terminal and in this order:
npm run facilitatornpm run merchantnpm run buybuy prints the payment hash. Then, while the payment is in escrow:
npm run settle -- <action> <paymentHash> [amount]where action is payment to read it, payerAccept, merchantRefund <amount>, dispute <amount>, captureIfUnchallenged or executeRuling. Amounts are in the token's base units, as in the 402: the demo's price of 0.01 USDC is 10000. dispute pins the agreement from demo/payments/ and prints the Court case.
- src/KlerosCaptureAuthorizer.sol: the operator, with its Foundry tests under test/.
- template/: dispute template, data mappings and the juror policy.
- script/: the deploy scripts.
- sdk/: the TypeScript SDK for merchants and buyers, with its own README.
- demo/: the rails of the demo, x402's facilitator and merchant server built from
main, and the buyer and settlement scripts on the SDK. - lib/: the submodules, commerce-payments and x402.
Requires Foundry and Node 22.18 or later for the SDK.
git clone --recursive git@github.com:kleros/kleros-x402.gitIf already cloned without --recursive:
git submodule update --init --recursiveforge buildforge testcd sdk && npm install && npm test && npm run buildThe demo links the SDK's build. Its own build compiles the x402 packages from the submodule, about a minute the first time:
cd demo && npm run build:x402 && npm install && npm testforge script script/DeployEscrow.s.sol --rpc-url $ARBITRUM_SEPOLIA_RPC --private-key $DEPLOYER_PK --broadcast --verify --etherscan-api-key $ETHERSCAN_KEYdeploys the escrow and its ERC-3009 collector; the escrow's constructor also deploys a TokenStore implementation, verified separately with forge verify-contract. Put the escrow address into ESCROW in script/DeployOperator.s.sol, then
forge script script/DeployOperator.s.sol --rpc-url $ARBITRUM_SEPOLIA_RPC --private-key $DEPLOYER_PK --broadcast --verify --etherscan-api-key $ETHERSCAN_KEYdeploys the operator, registering the dispute template from template/. Verification needs a recent Foundry, one that uses Etherscan's V2 API: 1.5.1 works, 1.1.0 does not. After a deploy, the addresses go into sdk/src/deployments.ts.