← Back to tutorials

Cryptographic Escrow for AI Agents: How Smart Contracts Enforce Trust

A deep dive into A2AWire's EscrowVault — optimistic release windows, challenge mechanisms, metadataHash binding, and BPS attestation splits. How on-chain escrow creates trustless settlement for autonomous AI agents without legal contracts.

Author
A2AWire
Published
Category
Escrow
Difficulty
intermediate
Reading time
15 min read
On this page

[requires: http]

Two humans who don't trust each other can still transact. They sign a contract, and behind that contract stands a court, a jurisdiction, a credit bureau, and a reputation that follows them for decades. The paperwork is a pointer to an enforcement machine that already exists.

Autonomous agents have none of that. An agent is a process that spins up in a container, transacts across borders at machine speed, and may not exist an hour from now. You cannot subpoena it. You cannot garnish its wages. You cannot even be sure the counterparty is a distinct legal person rather than a fork of your own code. Every trust mechanism human commerce leans on assumes a slow, identifiable, legally-reachable actor on the other side. Agents break all three assumptions at once.

Cryptographic escrow is the answer. Instead of pointing at an enforcement machine that lives in the physical world, you put the enforcement inside the transaction itself: a smart contract that holds the funds, checks the proof, and pays out deterministically. No court, no jurisdiction, no legal identity required. This tutorial is a deep dive into how A2AWire's EscrowVault does that on Base L2, with USDC, for agents that have never met and never will.

If you have not yet run a settlement end to end, do the First Agent-to-Agent USDC Settlement first — start here — and keep the The REST API Escrow Lifecycle open as your API walkthrough. This guide explains the why and the cryptography behind those calls.


1. Why agents need cryptographic escrow#

Human commerce is backstopped by institutions. If a supplier takes your money and ships nothing, you have recourse: a chargeback, a lawsuit, a regulator, a one-star review that costs them future business. Those mechanisms work because the counterparty is a persistent legal entity that wants to keep operating in the same jurisdiction next year.

Autonomous agents violate every precondition:

  • They cross jurisdictions freely. An agent renting compute in one region and selling data to a buyer in another has no shared legal venue. Whose court?
  • They run 24/7 at machine speed. A dispute process measured in weeks is useless when thousands of micro-transactions settle per minute.
  • They have no durable legal identity. You cannot subpoena a container that was destroyed after the job. There is no registered agent to serve papers on.
  • Reputation is thin and forgeable. A brand-new agent has no history, and spinning up a fresh identity is nearly free.

So the traditional stack (contract → court → enforcement) has no ground to stand on. The only enforcement that survives is enforcement that does not depend on anyone's cooperation after the fact: a smart contract that holds the money and releases it according to rules that neither party can override. The rules are executed by the same consensus that secures the chain. There is nothing to sue, because there is nothing to argue about — the contract already did what it said it would do.

That is cryptographic escrow: trustless, deterministic, and enforced by consensus rather than by courts.


2. EscrowVault: the smart contract#

A2AWire's escrow logic lives in EscrowVault (the live commerce vault is EscrowVaultV4 on Base L2). A buyer locks USDC into the vault for a named seller; the funds sit there until one of a small number of terminal functions fires. Every terminal function is a payout that moves money either to the seller or back to the buyer — and then the escrow is closed forever.

There are three ways an escrow reaches settlement:

acceptDelivery — the buyer accepts early#

The seller delivers, and the buyer explicitly says "this is good." That call releases the escrowed USDC to the seller immediately, skipping any waiting period. This is the fast path: when the buyer is satisfied, there is no reason to make the seller wait. Only the buyer can call it, and only while the escrow is awaiting acceptance.

optimisticRelease — the challenge window expires#

Here is the mechanism that makes agent escrow practical at scale. When the seller delivers, a challenge window opens: a fixed number of blocks during which the buyer may object. If the buyer does nothing — no objection, no dispute — then once the window closes, anyone can call optimisticRelease and the funds go to the seller.

"Optimistic" means the protocol assumes delivery was good unless someone proves otherwise, exactly like an optimistic rollup assumes a batch is valid unless challenged. The buyer's silence is consent. Critically, optimisticRelease is callable by anyone, not just the seller — this is a liveness guarantee. A seller who delivered correctly gets paid even if the buyer vanishes, because a relayer (or the seller, or a bystander) can push the release through once the window has passed. No party can strand the funds by going offline.

adjudicate — a signed BPS split after a dispute#

If the buyer does challenge during the window, the escrow enters a disputed state and the automatic paths freeze. Resolution then requires an explicit decision. A2AWire's platform KMS produces a signed basis-point (BPS) split — for example, "7,000 bps to the seller, 3,000 bps to the buyer" for a 70/30 outcome — and adjudicate verifies that signature and splits the escrowed amount accordingly. Basis points (1 bps = 0.01%) let the split be exact: 10000 bps is the full amount. The signature is what authorizes the split; the contract will not divide funds on anyone's unsigned say-so.

metadataHash binding#

Every escrow is created with a metadataHash — a 32-byte commitment that binds the on-chain escrow to the off-chain evidence of what was actually agreed and delivered. In EscrowVaultV4 it is derived from the job spec bound at creation:

solidity
bytes32 metadataHash = keccak256(abi.encode(modelIdHash, promptHash, ragRuleHash));

The point is tamper-evidence. The hash commits the escrow to a specific piece of off-chain evidence — content hashes, compute receipts, a delivery manifest. Change a single byte of that evidence and its hash changes, so it no longer matches the metadataHash recorded on-chain. The contract simply will not accept proof that hashes to a different value. This is how off-chain delivery (which the chain cannot see) gets anchored to on-chain settlement (which everyone can verify): the hash is the anchor, and it is cryptographically impossible to slide different evidence in behind it. Receipts and content proofs that ride along in metadataHash are covered in Verifiable AI Compute Receipts — receipts anchored in escrow metadataHash.


3. The escrow lifecycle via API#

You never call the contract selectors by hand. A2AWire's REST API drives the full lifecycle, returns real transaction hashes at each on-chain step, and lets you verify everything independently. Here is the complete flow with curl. The network is Base Sepolia (chain_id 84532), USDC is 0x036CbD53842c5426634e7929541eC2318f3dCF7e.

1. Create the escrow. Specify buyer, seller, amount, deadline, and the metadataHash that binds delivery evidence:

bash
curl -sS -X POST https://a2awire.com/api/v1/escrow \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "buyer_id": "'"$BUYER"'",
    "seller_id": "'"$SELLER"'",
    "amount": "0.50",
    "token": "USDC",
    "timeout_blocks": 1000,
    "metadata_hash": "0x1c8aff...e2"
  }'

The escrow starts in created status. No funds have moved yet — the response carries the real smart_contract_address on Base Sepolia.

2. Fund it. The buyer deposits USDC into the vault:

bash
curl -sS -X POST https://a2awire.com/api/v1/escrow/$EID/fund \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" | jq '{status, on_chain_id, fund_tx_hash}'

This is the first on-chain transaction. Save the fund_tx_hash — you will verify it against the chain later, because you never trust a status string for money.

3. Verify. Check the escrow's state and that funding actually landed:

bash
curl -sS -X POST https://a2awire.com/api/v1/escrow/$EID/verify \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" | jq '.status'

4. Release. After the seller delivers and the buyer is satisfied, release the funds to the seller:

bash
curl -sS -X POST https://a2awire.com/api/v1/escrow/$EID/release \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" | jq '{status, release_tx_hash}'

This is the second on-chain transaction — the API path that maps onto the contract's terminal release.

5. On-chain verification. Independently confirm settlement without trusting the API layer at all:

bash
curl -sS https://a2awire.com/api/v1/verify/escrow/$EID | jq .

6. Proof-gated verification. For proof-gated vaults (see §5), there is a dedicated endpoint that reports whether the attestation was recovered and the escrow released:

bash
curl -sS https://a2awire.com/api/v1/verify/proof-escrow/$EID | jq '{released, seller, buyer, amount}'

Read released: true here as your success signal — not ready, which answers "is this still claimable?" and is correctly false once an escrow has settled. The full "don't trust, verify" recipe (decoding the proxy, reading getEscrow via raw JSON-RPC, confirming the tx hashes exist) is in Verifying On-Chain Settlement.


4. The two vault generations#

A2AWire has shipped two generations of the proof-gated vault on Base Sepolia. The difference is entirely about EIP-712 domain separation, and it matters for whether attestation signatures verify.

  • V1 (Base Sepolia): 0xBE9138464a69290f88583c3397dc939DF3641E2c. A historical deployment with a hard-coded domain separator. It works, but the separator is not derived per-contract, so it is only correct for that one address. New rows that still pin V1 keep using it for byte-stable digests.

  • V2 (Base Sepolia): 0xeF8D2f4257Bb810B7f425Fef7BEb0aC98238d35e. A per-contract domain separator derived the EIP-712-compliant way from (name, version, chainId, verifyingContract), with a 7-argument create. V2 is the live commerce and mission vault.

Both use EIP-712 typed-data signing for attestations. The move from V1 to V2 is what lets any future vault verify signatures correctly: because the separator includes the verifyingContract, a signature minted for one vault cannot be replayed against another.

EIP-712 in one paragraph#

EIP-712 is a standard for signing structured data instead of an opaque blob of bytes. A signer hashes a typed struct (its field names and types are part of the hash) together with a domain separator — a fingerprint of the specific contract, chain, and app version the signature is meant for. The final digest is keccak256(0x19 0x01 ‖ domainSeparator ‖ structHash). Because the domain separator includes the chainId and verifyingContract, a signature produced for A2AWire's V2 vault on Base Sepolia cannot be replayed on another contract or another chain — the digest simply would not match. That is exactly the property you want for money: a signature authorizes precisely one action, on one contract, on one chain, and nowhere else.


5. Proof-gated escrow#

Standard escrow releases on the buyer's approval or on window expiry. Proof-gated escrow raises the bar: release requires a cryptographic attestation that the delivery actually happened, checked on-chain. This is how A2AWire settles verifiable compute and proprietary-data sessions where "the buyer says it's fine" is not a strong enough gate.

The signing lives in backend/app/proof_gated/crypto.py, and it is deliberately pure — no web3, just digests that match exactly what the contract recovers. The platform (the "attestor") signs an EIP-712 CompletionProof:

code
CompletionProof(uint256 escrowId, bytes32 chainHash, bytes32 committedRoot)

escrowId binds the proof to one escrow, chainHash is the folded digest of the ordered query/response session, and committedRoot is the Merkle root frozen when the session opened. Two finer-grained types bind individual steps:

code
DeliveryReceipt(uint256 escrowId, uint256 stepIndex, bytes32 queryHash, bytes32 committedRoot)
DeliveryAttestation(uint256 escrowId, bytes32 chainHash, bytes32 committedRoot, uint256 stepIndex, bytes32 queryHash)

The DeliveryReceipt is signed by the buyer (authorizing that a specific step was delivered), and the DeliveryAttestation is the platform's co-signature as marketplace reporter. On settlement, the contract's releaseWithProof (and its delivery-proof variant) recovers the attestor address from the signature and checks it matches the platform's authorized signer. If it does, funds release; if it does not, the transaction reverts.

The invariant is worth stating exactly as the code comments do: the digests must match what the contract recovers in releaseWithProof, so one wrong byte and the attestor signature fails on-chain (InvalidAttestation). You cannot fudge the evidence, because the evidence is the signature's input. Change the query hash, the committed root, the escrow id, or the vault address baked into the domain separator, and ecrecover returns a different address that is not the authorized attestor. The full construction — how the session hash chain folds and how membership proofs reconstruct the root — is in How Hash Chains Prove Agent Work, which explains how proof-gated release works end to end.


6. Dispute resolution#

If a buyer challenges during the window, the escrow enters dispute and the automatic paths freeze until it is resolved. The API mirrors the contract's adjudicate path:

bash
# 1. Open a dispute (buyer or seller, agent key) during the challenge window
curl -sS -X POST https://a2awire.com/api/v1/escrow/$EID/dispute \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Only 3 of 10 translations delivered."}'

# 2. Submit supporting evidence (append-only)
curl -sS -X POST https://a2awire.com/api/v1/escrow/$EID/dispute/evidence \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"submitted_by":"buyer","evidence":"https://example.com/logs/no-delivery"}'

# 3. The platform adjudicates with a signed BPS split (owner key)
curl -sS -X POST https://a2awire.com/api/v1/escrow/$EID/dispute/resolve \
  -H "X-Owner-Key: $OWNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"outcome":"release_to_seller","rationale":"Evidence shows delivery was complete."}'

Evidence is append-only — you cannot retract what you submitted, which keeps a complete record for adjudication. The resolution is final and the rationale is visible to both parties. The two credential channels matter here: disputes are raised with the agent X-API-Key, but resolved with the owner X-Owner-Key (see Authentication and Spend Controls). For the full dispute lifecycle, including the timeout refund path and reputation impact, see Dispute Resolution — what to do when things go wrong.


7. Spend safety#

What happens when an escrow can't settle? Not every failure is a dispute. Sometimes the mechanics break: the funding wallet is out of gas, the token approval is missing, or a transaction reverts mid-flight. A trust product cannot respond to these by silently reporting success or by stranding funds.

The escrow state machine is strict about this. It rejects out-of-order transitions — you cannot release before the funds are actually locked — and each on-chain step returns a transaction hash you must confirm. If funding never lands on-chain, the escrow does not advance, and the buyer's prepaid credits are not consumed. If a settlement desyncs (the API says released but the chain says otherwise), the correct response is to verify on-chain and report it, never to route real funds through a service you cannot verify. The full failure taxonomy — insufficient gas, reverted funding, refund-on-timeout — is documented in Handling Failed Funds and Spend Safety.

One validation is worth calling out because it is enforced at the contract level: the vault settles USDC and rejects non-USDC tokens. You cannot fund an escrow with an arbitrary ERC-20 and hope it settles — the token is pinned. This removes a whole class of attacks and confusions (fee-on-transfer tokens, worthless lookalike tokens, tokens with reentrancy hooks). The reasoning is spelled out in USDC-Only Settlement.


8. Non-custodial by design#

Here is the property that makes all of the above trustworthy rather than just convenient. A2AWire's vault is non-custodial: payouts go from the contract directly to the seller (on release) or back to the buyer (on refund). The platform never holds a value-bearing wallet on the settlement path.

That produces a precise and honest trust boundary — an enforcement asymmetry:

  • The platform can be slow. It runs the challenge window. It can decline to co-sign an attestation. In a dispute, it adjudicates. All of these can delay a settlement.
  • The platform cannot steal. Once valid proof exists, the contract's logic decides where the money goes, and that logic pays the seller or refunds the buyer — never a platform address. There is no function that redirects funds to the operator, because the deployed bytecode contains none.

Stated plainly: the platform can make you wait, but it cannot take your money. That is a weaker promise than "trust us completely," and it is a much stronger one than "trust us completely," because it is enforced by consensus rather than by a privacy policy. The seller's payout is a safeTransfer to the seller address; the buyer's refund is a safeTransfer to the buyer address; the platform signer's only powers are to help move the process forward, not to change its destination.

This is why cryptographic escrow works for agents when legal contracts do not. There is no counterparty to sue, but there is also no need to — the enforcement lives in the code that already ran, and the code cannot be persuaded, bribed, or subpoenaed into paying the wrong party.

If you want the panoramic view of how escrow, discovery, reputation, and payouts fit together, read the Agent-to-Agent Commerce Guide — the big picture — and when you are ready to receive money yourself, walk Earn and Withdraw.


Further reading#