
Recourse
x402 pays before delivery. Recourse escrows it and refunds you when the API breaks its SLA.
Demo
About
When an agent pays for an API call with x402, the money moves before the response exists. If what comes back is empty, stale, or quietly wrong, it's already gone. A person notices and stops buying from that seller. An agent making a thousand calls an hour doesn't notice, and has no refund path to reach for. Recourse changes one thing. The payment goes into an escrow contract on Hedera instead of straight to the seller. Before any money moves, the seller publishes an SLA: a short list of checks a machine can run on the response, like "bid must be greater than zero" or "the spread must be within 50 basis points". The 402 response carries the hash of that document, and the buyer refuses to pay if the terms don't hash to what was advertised. The seller returns the data along with a signature over what it sent. If the response breaks one of the published checks, the buyer disputes and posts a bond worth 10% of the payment. A Chainlink CRE workflow running inside an AWS Nitro enclave then reads the response, checks it against the SLA, and rules on it. The payload never goes on-chain and no node operator sees it. That matters, because the thing being argued over is usually the exact thing the buyer paid to keep private. The verdict arrives back on Hedera carrying a reason code that names the clause that failed. Reason 102 means clause 2 broke. So a refund points at a specific promise the seller made and didn't keep, rather than at somebody's opinion about data quality. If nobody disputes, none of that happens. The window closes, the seller withdraws, and the whole exchange costs one extra contract call with no oracle involved. Disputes are the exception, and only the exception pays for the machinery. It runs end to end on testnet today. In one run the seller returned a negative bid, and the buyer got 0.11 HBAR back: the payment plus the bond. In another the seller behaved and was paid 0.1 HBAR with no adjudication at all.
How it's made
The escrow is Solidity on the Hedera testnet EVM, built with Foundry, with 40 tests including a fuzz test on the accounting. The SLA checker is a small TypeScript package with canonical JSON hashing so the buyer, the seller and the enclave all agree on what a document hashes to. The adjudicator is a Chainlink CRE confidential workflow. The site is Next.js, and it reads the chain directly rather than from a database, so nothing on it can drift from what actually happened. Payments settle through Blocky402 on Hedera. Three things forced the design into a shape we didn't expect. First, Hedera doesn't run contract code on a plain transfer. A native TransferTransaction credits a contract's balance without ever calling receive() or fallback(), and the x402 facilitator settles that way. So the escrow can't be told about its own funding. We split it into two steps: the facilitator settles, then anyone calls bind() to attach terms, and the contract only commits money it can prove it already holds by checking its balance against what it has already promised out. One deposit can never back two payments. That invariant is the thing the fuzz test protects. Second, CRE can't write to Hedera. It isn't in the 57 chains a tenant can target, so there's no forwarder for writeReport to reach. The enclave writes its verdict to a relay contract on Ethereum Sepolia instead, where the KeystoneForwarder checks the DON signatures, and CCIP carries it the rest of the way to Hedera. The escrow only accepts a message from the CCIP router with that relay as the sender. We confirmed the lane by calling isChainSupported on the live Sepolia router, which was necessary because Chainlink's own docs leave Hedera off the destination list. Third, we wanted an LLM to judge disputes and couldn't. The enclave result is attested and verified by DON consensus, which needs the same input to produce the same output every time, and a model doesn't. So the model moved upstream to help sellers write the SLA, and the enclave enforces a contract both sides already agreed to. There isn't even a clock read in the handler, because Date.now() would break consensus. Freshness is judged against a deadline that's already on-chain. The most expensive bug: our first live verdict reached Hedera and did nothing. CCIP said the message was delivered, the OffRamp transaction succeeded, and the escrow never moved. Our trimmed CCIP receiver was missing supportsInterface. The router staticcalls it before delivering, and an address that reverts gets treated as a normal account, so the router skips the receiver and still records the message as executed. Nothing reports a failure anywhere. The verdict just disappears. Two tests now pin that, including the literal interface id the router looks for. Two smaller ones worth passing on. Blocky402 answers HTTP 200 for a payment that failed, with the real result in the body, so our gateway gated on the status code and briefly gave away paid work for free. And cre workflow simulate --broadcast submits real transactions without needing deployment access, which is how we produced genuine on-chain state while our CRE org was still gated.