Back to News & Insights
Artificial Intelligence September 6, 2026 · 6 min read

The receipt should come from the person who received it

This is a submission for Weekend Challenge: Generosity Edition What I Built Every...

The receipt should come from the person who received it

Every donation platform proves the same single fact: money left your account. The fact you actually care about is the second one, that it reached a person, and the receipt for the first is issued by the organisation holding the money. The thing you're trusting and the thing producing the evidence are the same thing.

You lock SOL in an escrow no intermediary can open. The only thing that moves it is a signature from the recipient's own wallet, and that transaction carries the hash of a recording where they say, in their own words, what they received. The receipt comes from the person at the end of the chain, and the chain welds it to the transfer.

cairn verify re-downloads the recording, re-hashes the bytes it gets back, rebuilds the receipt from scratch and compares the result against what's on chain. It doesn't ask my server to be honest. With cairn hash it doesn't need my server to exist.

A cairn is a stack of stones travellers raise along a route. Each person who passes adds one. It proves somebody came through, it guides whoever comes next, and it works with nobody owning it and nobody supervising it. Every clause maps onto something the software does, which is the only reason the name stuck.

docs/COMMANDS.md walks the whole loop with real captured terminal output at every step — funding an escrow, recording a receipt, releasing, and verifying.

Those two hashes were produced by different code paths, minutes apart. The same command against a recording whose bytes have been altered:

Two things here look similar and are not, and the entire design is the consequence of separating them.

Authorization is an ed25519 signature from the recipient key the donor names at creation. The program enforces it. No server key, operator key or admin override substitutes for it.

Testimony is the recording. Human evidence, hashed and committed on chain so it's tamper-evident and bound to exactly one transaction signature.

The recording authorizes nothing. Audio used as an authentication factor is walked through by replay or a decent TTS model, so Cairn treats it as attached evidence with guaranteed integrity instead. That's a much smaller claim, and it's one the software can actually keep.

Note the asymmetry. The recipient's way out is a signature; the donor's way back is a deadline. There's no third arrow and nobody standing in the middle.

Strip Cairn down and a single sentence is left: two independently computed hashes agree. Everything below exists to make that sentence hard to fake and easy to check.

locale is a fixed 8 bytes rather than a string, and that's the load-bearing choice. Every field except the transcript is fixed width, so for a preimage of length L the transcript is always exactly L - 96 bytes. The encoding is injective with no length prefixes anywhere: no two distinct receipts concatenate to identical bytes. Add a second variable-length field and that evaporates — ("ab", "c") and ("a", "bc") land on the same preimage, and two different receipts share one hash. A test named transcriptandlocaledonotsmear exists purely to fail if someone later decides locale should be a String. Transcripts are NFC-normalised, trimmed and whitespace-collapsed first, so the same recording hashes the same regardless of which machine composed the receipt.

Then there's the question of how many implementations of that function exist, and the answer is one. cairn-core compiles to both sbf-solana-solana and the host, so the on-chain program, the CLI and the API server all call the same receipthash(). Not kept in sync, not mirrored, not generated from a shared schema — the same function, compiled twice. The account layout works the same way: Escrow is defined once in cairn-core and the program wraps it in a newtype that Anchor's traits hang off, so the bytes the program writes are the bytes the indexer reads, with a test asserting that Anchor's generated discriminator matches the constant the decoder uses.

That count is also why the client is a CLI. A browser would need its own receipthash(), which means hand-porting the normalisation into TypeScript, and JavaScript's \s and Rust's char::iswhitespace disagree about U+FEFF — a byte-order mark in a transcript gives you a browser reporting a valid receipt and a chain reporting a fake one. That character is patchable; the shape of it isn't. Any second implementation is a second answer, and an interface whose job is to display "these match" while running different code from the thing doing the matching argues against itself.

With one function to check against, three commands do the checking rather than taking anything on faith. cairn show re-hashes the description client-side against the on-chain needhash, so the API can serve whatever text it likes and it doesn't matter. cairn receive rebuilds the receipt from the audio on the recipient's own disk and refuses to sign if the server's hash doesn't cover it. cairn hash touches no network, needs no key and reads no database — compute a hash with the wifi off and compare it against a block explorer.

Which leaves the server holding nothing worth stealing: no keypair, no signing path, no instruction it could call to move a lamport.

Transcription runs through ElevenLabs Scribe. The recipient speaks whatever language they actually speak, and the transcript joins the canonical receipt alongside the locale tag.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation