6. Proof before release
With escrow, the seller is paid only after it writes a result hash on chain: SubmitResult sets
result_hash in the escrow datum to the SHA-256 of the response body it delivered. That is a public,
signed commitment. ZyroPay holds it against an independent measurement: the MCP worker computes its
own SHA-256 over the exact bytes your agent received, without trusting any hash the seller sent.
Whenever the API reads an escrow, it compares the two and returns the verdict as x402.proof on the
request and escrow.proof on the escrow: {state, receivedHash, onChainHash, resultTx, checkedAt, message}. The verdict is stored, and each change of state is audited once.
| State | Means | What you can do |
|---|---|---|
pending |
No on-chain result yet, or no delivery report yet | Wait, or request a refund |
verified |
The SHA-256 of what your agent received equals the on-chain result_hash |
Nothing: the seller collects after unlockTime |
mismatch |
They differ: the seller recorded one result and delivered another | Recommended: request a refund |
not_delivered |
submitResultTime passed with no result on chain |
Recommended: withdraw your refund |
unverifiable |
The seller recorded a result, but the body could not be hashed (truncated or not text) | Judge for yourself; a refund is still possible |
receipt_only |
A direct payment: nothing is recorded on chain, only the receipt speaks | Nothing to release |
On mismatch or not_delivered, escrow.refund.recommended names the refund to take
(request_refund, or withdraw_refund once it is possible) and refund.reason says why. The audit
events are x402.proof_verified, security.x402_proof_mismatch, x402.proof_not_delivered and
x402.proof_unverifiable.
Proven live: verified
Section titled “Proven live: verified”The escrow payment on the previous page delivered a report; the seller then
recorded its hash on chain, and ZyroPay’s received hash was equal: verified.
Open ee7b142e73…865361 on Cardanoscan (preprod) and check:
| Where | What you should see |
|---|---|
| Inputs | The escrow UTxO 630f2012…#0 (5 ADA, script) and a 20 ADA seller UTxO |
| Script | One Plutus V3 execution (the 10,003-byte vested_pay script, inline) |
| Output #0 | 5 ADA back to the escrow, same datum except the fields below |
| Datum: result_hash | d80a4d02d13d08a45d4662e432c5a5a6550ac9c2a093549f5dbbd0c6afbfbfce |
| Datum: state | ResultSubmitted (constructor 1); seller cooldown set |
| Collateral | The same 20 ADA seller UTxO, with a 15 ADA collateral return |
| Fee | 0.688128 ADA (11,052-byte transaction, paid by the seller) |
The hash in the datum, d80a4d02d13d08a45d4662e432c5a5a6550ac9c2a093549f5dbbd0c6afbfbfce, is the
same value the seller’s delivery receipt signs as contentSha256, so the receipt and the chain
agree with each other as well as with the buyer.
A live mismatch, using the seller’s deliberately dishonest demo mode, is
awaiting its run. The comparison and every state are covered
offline Tested offline.
What the proof does not prove
Section titled “What the proof does not prove”- That the content is good. A seller that commits to a useless report and delivers exactly that
report is
verified. The proof is about integrity: you received what the seller committed to. - Anything before the escrow is read. The verdict is computed when the API reads the escrow, for example when you or the agent open the request; nothing pushes it to you.
- Release. ZyroPay does not block the seller’s withdrawal on chain: after
unlockTimethe script lets a seller with a result withdraw unless a refund was requested. The proof tells you, before that time, whether to request one.
Code: apps/api/src/services/escrow-view.ts, packages/shared/src/domain/requests.ts
(X402ProofStateSchema), apps/mcp/src/mcp/x402-delivery.ts.