Docs › Verifiable runs: prove what your agent did
Verifiable runs: prove what your agent did
When an agent ran unattended, "it worked" is a claim. A proof is better: a record of what ran, when, with which evidence and at what cost, fixed the moment the run ended, chained to every other run of that day, and anchored in the Bitcoin blockchain. Later you, an auditor or a customer can check that the record was not edited afterwards, without asking us. This page explains exactly what is hashed, what is not, and where the guarantee stops.
What is in the record
At POST /v1/runs/end RunVouch builds one JSON object for the run and stores its sha256 (the leaf). The object has these keys and nothing else:
run_id, agent, account_id, started, ended, status, cost, tokens, tool_calls, output_bytes, evidence (name -> true/false), evidence_ok, source, exit (only when reported), tool_events_hash
tool_events_hash is the sha256 over the ordered list of tool events of the run: tool name, the input hash the client sent, ok flag and timestamp. The leaf is sha256(canonical JSON), canonical meaning sorted keys, separators , and :, UTF-8. It is written once and never updated.
What is not in it: prompts, model outputs, tool inputs, file contents, log lines. RunVouch never receives those, so they cannot be hashed. The proof shows that a run with these numbers and these evidence booleans ended at this time; it does not show what the agent wrote.
The daily chain
Every UTC day, a few minutes after midnight, the server seals the day that just ended:
- Take the leaves of all runs that ended that day (all accounts, sorted by run_id).
- Build a Merkle tree: pairwise
sha256(left + right)over the hex strings, an odd node is paired with itself, an empty day givessha256(""). The top is the day root. chain_hash = sha256(prev + ":" + date + ":" + root), whereprevis the chain hash of the previous sealed day (64 zeros for the first one).- Write
/proof/days/DATE.jsonwith date, root, prev, chain_hash and the list of (run_id, leaf), publicly, without authentication.
The chain is global on purpose: one public object per day for everyone, so changing one leaf of one account would change the root that every other customer can also see. The index of all sealed days is at api.runvouch.com/proof/. Run ids are random hex; the day file does not reveal who ran what.
The Bitcoin anchor
Right after writing the day file the server runs ots stamp DATE.json from the OpenTimestamps client. That submits the file hash to the public OpenTimestamps calendar servers, which aggregate hashes and commit them in a Bitcoin transaction. The resulting DATE.json.ots is served next to the day file. The status you see in ots_status means:
| ots_status | meaning |
|---|---|
pending | stamped; the calendar has the hash, the Bitcoin block is not in yet. Usually a few hours. |
bitcoin:NNNNNN | the .ots file was upgraded and contains an attestation from Bitcoin block NNNNNN. |
ots missing / stamp failed: ... | the chain still works, but that day has no Bitcoin anchor. We do not hide this. |
Once a day the server runs ots upgrade on pending files of the last two weeks. To check an anchor yourself: pip install opentimestamps-client, download both files, run ots verify DATE.json.ots -f DATE.json. With a local Bitcoin node it verifies against the block header; without one it tells you which block to look up.
Get the proof of a run
rv proof RUN_ID # prints the proof JSON rv proof RUN_ID --verify # recomputes leaf and Merkle path, fetches the public day file, exit 0 or 1 curl -H "X-API-Key: rv_..." https://api.runvouch.com/v1/runs/RUN_ID/proof
The response contains record, leaf_hash, stored_leaf_hash (the one written at run end; a difference would mean the row changed since), merkle_path as a list of (sibling hash, side), root, chain_hash, prev, sealed, ots_status and verify_url. The dashboard has a proof link per run, and the MCP server has runvouch_run_proof, so an agent can fetch its own proof and hand it to whoever asked for the work.
Verify offline, without our code
templates/verify_proof.py is a standalone Python 3 script, standard library only. Save the proof JSON and the day file, then:
python3 verify_proof.py proof.json 2026-08-26.json PASS leaf hash matches the record PASS merkle path leads to the root PASS day file lists this run with this leaf PASS day file root recomputed from its leaves PASS chain hash of the day VERIFIED
Change one byte of the record and the first line reads FAIL and the exit code is 1. The script fetches the day file itself when you leave the second argument out. It checks everything except the Bitcoin attestation; that is the ots verify step above.
Honest limits
- A day is sealed after it ends (UTC) and anchored some hours later. For a run that ended today, the proof endpoint returns a live root and
sealed: false; until midnight it is our word, not a proof. - The leaf is computed from what your client reported. If the client lies about cost or evidence, the proof faithfully preserves the lie. What it rules out is editing afterwards, by you or by us.
- A run that never called
/v1/runs/end(stalled, killed) has no leaf and is not in any day. - The record contains no content. It cannot prove that the report your agent wrote said X; it proves that a run with evidence
report_written: trueended at that time and cost that much. - OpenTimestamps gives a proof that the day file existed before a certain Bitcoin block. It does not give a proof of "not after". The chain hash gives the ordering between days.
- If the ots client is missing or a calendar is down, the day is sealed without an anchor and says so. We do not backfill anchors silently.
- Runs that ended before 26 August 2026 (the day this shipped) got their leaf computed afterwards from the stored fields, not at the moment they ended. From that day on the leaf is written in the same transaction that ends the run. The first sealed day is 2026-08-25.
Need a key? Get a free key · Stuck? contact