Bothy: Building Verifiable AI Decisions With Arweave Receipts

The decision with a name on it
Every winter, somewhere in the UK, a person with a pager has to decide whether to close a mountain road. Picture a Cumbria County Council winter duty officer with the A5094 and the B5311 on their desk: narrow, steep, single-lane passes that ice fast and plough badly. Close too early and you strand a valley of commuters and farms. Close too late and a family of four meets a snowdrift on a blind bend.
The Met Office warnings, the gritting schedule, the gradient map and the incident history all exist, scattered across systems that were never joined. The officer has to hold it all in their head under a deadline and be personally accountable for the call.
That is the decision Bothy is built for. It is an open-source, evidence-first agent. It watches a curated evidence graph (Met Office warnings, live weather, route topology, incident records) and produces a cited risk assessment: a score, a label, a draft for the duty officer, and the reasoning chain behind every claim, each linked to a real data point. Then it does what dashboards don't: it hands the decision to a named human, who approves or rejects it, and it keeps the whole trail.
The same agent has a second surface. For supply-chain analysts it produces defence briefs, cited investigations of what would happen to a logistics programme if a dependency went dark, reviewed by a named reviewer before anything counts as decided.

The accountability gap
The interesting problem here isn't the AI. It's what happens after.
Every "AI safety" feature in every dashboard I've seen asks you to take the vendor's word for it. The model says it cited. The platform says it audited. The log says the approval happened. But when a duty officer's name goes on a closure, or an analyst's name goes on a risk call, the question that matters is: can anyone independently verify that this exact output existed at this exact time, and that nobody edited it before the human signed?
Usually the evidence behind a decision evaporates. Logs rotate, dashboards version, decisions get re-edited. So my answer this week was to stop asking for trust and start publishing fingerprints.
At the two moments that matter, when the agent generates an output and when a human decides on it, Bothy anchors a SHA-256 receipt to Arweave mainnet. The record itself never leaves our systems. Only its fingerprint does, permanently.
A real receipt
Here is one minted by the production deployment:
- Assessment: a-flood-r-A6108-1793402244148
- Receipt: arweave.net/uy2raHTZgIsTw7KG9ZabR4wYQ1V_7tGp3SrrIwAT2NY, mined at block 2032288
Open the case link and click Verify. Your browser fetches the anchored receipt, recomputes the fingerprint of the record on screen with crypto.subtle, and compares the two. My server is not trusted for the answer, and that is the whole point.

Why two anchors instead of one
A receipt on generation proves the AI's output existed, unaltered, at time T. That is table stakes.
The second moment is the human decision. The decision receipt chains back to the generation receipt (subjectDigest plus generationTx), records the decision and its timestamp, and stores the reviewer's identity only as a hash: committed, not revealed. On the defence-brief side it also pins the head of the case's hash-chained audit log.
So the anchored statement isn't "an AI said X on Tuesday." It is "a named responsibility-holder signed off on X on Wednesday, and the audit trail behind it hashes to this value." If anyone later edits the brief, the draft or the log, recomputation fails loudly.
What the fingerprint actually covers
This was the subtle part. The naive version, sha256(JSON.stringify(assessment)), doesn't survive contact with a real database. Postgres real columns round-trip a few ulps off the in-memory value, jsonb doesn't preserve key order, and fields like status and decidedAt legitimately change after anchoring.
So the digest covers a canonical payload: immutable fields only, keys sorted recursively, scores rounded to four decimals.
// packages/shared/src/anchor.ts: one module, used by server and browser assessmentAnchorPayload(a) → { id, scenario, routeId, at, score: r4(a.score), label, confidence: r4(a.confidence), causalChain, evidence, draft, engine, responsibleActor, priority, } Because the agent and the browser share the same canonicalization module, a judge recomputing in devtools gets the same fingerprint the server anchored. It is tamper-evident by construction, not by promise.
The honest failure log
Three things went wrong on the way here, and they're worth more to readers than the wins.
- I built against a testnet that doesn't exist.
testnet.arweave.devwas in my first draft, and its DNS never resolved. Arweave has no public testnet in that shape; the ecosystem usesarlocallocally. That turned out to be the right forcing function. A roughly 300-byte receipt costs a fraction of a cent on mainnet, andarweave.net/<txId>is permanently resolvable, which is strictly better evidence than a testnet transaction nobody could check anyway. - The product's main button never anchored. The backend anchored on the plain
/assessendpoint, but the UI's actual "Run agent" action went through the streaming route, which had no anchoring and discarded the result. Anyone clicking through would have seen nothing. That is when anchoring moved from an API feature to a product feature: a persistedpending → anchored | failedstate, and the card you see now. - A shared Docker network ate my routes. In production behind Coolify's Traefik, our Caddy sat on a network where other apps answer to
webandidp, so half the requests 502'd to the wrong containers. The fix was to proxy by unique container names and document the project-name collision (-p bothy, always).

Where decentralization earns its name
- Arweave is the permanence layer. It is a storage proof anyone can recompute, on-chain, forever, for fractions of a cent, with no vendor in the verification loop.
- Inference is provider-plural. The agent uses an OpenAI-compatible chain (free-first: Qwen on HF Inference, OpenRouter's free tier, Nebius, any OAI base URL, local Ollama) that degrades to a deterministic scripted engine rather than dying. The receipt doesn't care which engine wrote the text; it pins the output.
- Nosana GPU deployment is next. The containerized agent and the
nosana.jsonjob definition are already in the repo, and credits were claimed and are pending as of this writing. The verification layer works identically whether inference runs on my VPS, a Nosana node, or your laptop.
Try it yourself
- Open bothy.trustfall.xyz/watch and run an assessment on a route. Watch the "Permanent public record" card flip from Anchoring to Anchored.
- Click Verify, or skip the UI entirely:
GET https://bothy.trustfall.xyz/api/anchors/assessment/<id>lists the receipts, andcanonicalJSONplussha256is about 30 lines you can run anywhere. - Read the code at github.com/thisyearnofear/bothy:
packages/shared/src/anchor.ts(canonical payloads),apps/agent/src/anchors.ts(the anchor store) andapps/web/components/AnchorCard.tsx(the card and in-browser verify).
The real close
The duty officer's problem isn't "AI." It's that when their name goes on a closure, the proof of why usually disappears. Bothy makes that impossible by design: the agent produces the draft, the human owns the call, and Arweave holds the receipt. The accountability belongs to the person. The proof belongs to nobody. It's just there, permanently, for anyone to check.