Inspiration

An agent buys 500 market-data records, promised generated within the last hour. The file arrives. Row count checks out. Schema parses. The seller's signature verifies. A signed timestamp over the file, issued by an independent authority, falls inside the window.

Every acceptance check passes. 100 USDC settles to the seller. Final.

251 of those 500 records were generated 26 hours earlier. The seller pulled yesterday's feed, put it into a new file this morning, and had that file timestamped honestly. Nothing was forged.

The payment was valid. The fulfilment wasn't.

Human adjudication does not scale to machine-frequency transactions worth cents. So we asked what could settle without one, and found this:

Some promises are cheaper to disprove than to prove.

A universal claim like every record was generated after 14:05 takes 500 checks to verify and one to falsify. That asymmetry is the whole project.

What it does

Recourse holds an agent's payment in escrow instead of sending it to the seller, and releases it only if nobody proves the delivery broke an objective promise.

Buyer and seller sign purchase terms. Each protected condition is bound to the exact phrase in the seller's listing that it represents. If a term cannot be expressed by the protocol's supported verification primitives, Recourse refuses to protect it before payment opens.

The buyer's agent scans the delivered payload locally, for free, and finds the first violating record. It submits that single counterexample on-chain. The chain sees one record instead of five hundred.

Two things make this more than escrow.

Authentic evidence can still prove the wrong claim. A signed timestamp over a delivered file establishes when the file existed. It says nothing about when the records inside it were generated. Recourse type-checks offered evidence against the claim the condition actually requires and rejects the mismatch before any money moves. In our demo the seller's timestamp is genuine, it verifies, and it is refused.

Some promises cannot be checked, and we say so. "High quality investment reports" maps to no supported condition, so no protected payment opens. The complete instruction set is four opcodes and it never grows to fit a term.

How we built it

Three Solidity files: one escrow contract holding the protocol, and two internal libraries that solc inlines into it. Four opcodes, no on-chain iteration, no strings, no dynamic interpretation. Anything not expressible in those four is unprotectable.

The upstream data source signs a Merkle commitment over per-record leaves, where each leaf binds the record's content hash to its generation timestamp in a single preimage. That binding is what stops a fresh timestamp being paired with stale content, and without it the entire proof system would be decorative.

A breach proof carries the record bytes, its index, its generation time, the source identifier, and a Merkle path. The contract rebuilds the leaf from those inputs, verifies inclusion against the committed root, checks the commitment names this purchase and this obligation, recovers the issuer's signature, confirms the claim type is one that bound leaves can establish, and confirms the predicate is actually violated. Then it refunds. Any failure reverts.

The protocol covers successful release, cryptographically proved breach, non-delivery, withholding, unsupported terms, and evidence-type mismatches.

40 Foundry tests. Six end-to-end scenarios against a local chain. An escrow deployed on Base Sepolia. A product surface where every displayed value comes from an actual run, including a live path: pressing Protect & Pay executes the real breach scenario and returns real transaction hashes.

Challenges we ran into

A seller could commit and then deliver nothing. A delivery commitment is 32 bytes of Merkle root. A breach proof needs record bytes, an index, a timestamp and a path, all of which come from a payload that never touches the chain. So a seller could submit a commitment, send the buyer nothing, wait out the challenge window and collect. Committing and withholding was strictly better for a dishonest seller than stalling.

We closed it with an availability challenge. The buyer names an index, and the seller must open that leaf within a cure period or the purchase refunds. The property that makes it work is that answering is self-incriminating: curing a challenge on a stale record hands the buyer the exact bytes it needs to build the refund proof. Answering honestly loses. Refusing loses.

Our own product overclaimed, twice. The protection panel originally badged a row-count condition as PROTECTED. What the contract establishes is that a permitted issuer signed a struct whose leaf count reads 500; between that integer and the buyer's inbox sit two unverified steps. We changed the label to ISSUER ATTESTED and added the caveat.

The page headline originally read payment becomes final only when the delivery keeps its promise. The protocol establishes something weaker: nobody proved otherwise inside the window. We changed the headline.

We nearly shipped a demo we couldn't defend. The first version deliberately corrupted the head of the delivery, which meant a judge could reasonably ask whether we planted the bad row at index zero. We rebuilt the dataset so the first 187 records are clean and the stale ones are interleaved after that, with the longest contiguous stale run being four. A buyer agent spot-checking the first page finds nothing wrong.

Accomplishments that we're proud of

The counterexample primitive. Universal claims settle by falsification, not verification. There is no ALL_ opcode, nothing iterates records on-chain, and a single O(log n) proof reverses a settlement. In our demo 251 of 500 records violate the promise, and exactly one was submitted.

The evidence-type check. Separating BLOB_EXISTENCE_TIME from RECORD_GENERATION_TIME is one require, and it is the difference between a system that checks something and a system that checks the right thing.

Test discipline. The content-binding security property is tested in both substitution directions with a positive control, so a rejection is attributable to the binding rather than to an unrelated failure. One security branch is unreachable through the public API, so we mutation-tested it: deleting the branch makes the test fail at exactly the next check in sequence, which proves the fixture reaches that branch rather than passing for the wrong reason.

Refusal as a feature. The system declines to protect what it cannot objectively verify, and says so in the buying flow before the agent pays.

What we learned

The hardest part was not the cryptography. It was noticing where our own product claimed more than the code established.

For a project whose entire thesis is that authentic evidence can prove the wrong claim, being caught overclaiming would have been the worst possible outcome. We caught ourselves four times during the build, and each correction made the submission smaller and more defensible.

We also learned where the boundary of the idea sits. Some promises are falsifiable, some are attestable, and some are neither, and a settlement system is more useful when it tells you which one you are looking at than when it treats them all the same.

What's next for Recourse

Each verification capability expands what agents can safely buy. Record freshness unlocks data feeds. Deterministic test outcomes unlock generated code. Latency receipts unlock inference and compute. Provenance unlocks training-data commerce.

Agent buyer protection already exists. x402r provides refundable payment infrastructure with arbiter-based delivery protection, TessPay demonstrates predicate-gated verify-then-pay settlement, Virtuals ACP uses evaluator agents, and RAILS specifies broader verification-native clearing. Recourse targets the subset none of them settle without a human or an evaluator in the loop: objectively falsifiable fulfilment claims.

Long term this belongs inside a rail like x402r rather than beside it. We are the verification layer, not another escrow.

Known limits, stated plainly: Recourse does not solve general data availability, and it does not eliminate the upstream source-of-record assumption. It removes the seller's ability to attest to its own performance. A seller withholding part of a delivery is raised in cost rather than fully closed, and the general answer there is proof of retrievability.

Built With

Share this project:

Updates