When an AI agent spends money, sends an email, or changes a record, someone eventually asks the same question: what exactly did it do, and can I trust this log? A normal log line can be edited after the fact. A receipt that is chained to the one before it cannot be edited quietly.
The idea
Each action gets a receipt. The receipt holds what was decided, the inputs the agent saw, who approved it, and a timestamp. It also holds the hash of the previous receipt. Change any old receipt and every hash after it stops matching.
import hashlib, json, time
def seal(prev_hash: str, action: dict) -> dict:
body = {"ts": time.time(), "prev": prev_hash, "action": action}
digest = hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()
return {**body, "hash": digest}
def verify(chain: list[dict]) -> bool:
prev = "genesis"
for r in chain:
body = {k: r[k] for k in ("ts", "prev", "action")}
ok = r["prev"] == prev and r["hash"] == hashlib.sha256(
json.dumps(body, sort_keys=True).encode()).hexdigest()
if not ok:
return False
prev = r["hash"]
return True
What to put in a receipt
- The decision and the options that were considered, not only the winner.
- The data the agent used (prices, documents, policy version) or a hash of it.
- The human or policy that approved it.
- The agent identity and the version of its instructions.
What it does not do
A hash chain shows that records were not changed after sealing. It does not prove the agent made a good decision, and it does not stop someone rewriting the whole chain from the start unless you anchor the latest hash somewhere you do not control (a timestamping service, a signed commit, a counterparty's copy). Keep that limit in mind when you describe it.
Where this shows up
Compliance rules for automated systems increasingly ask for logs of what a system did and why. Receipts like this are one way to keep those records in a form a third party can check without trusting you.
I write about agent audit trails and receipts for SYEN Systems (syensystems.com).
Top comments (1)
The whole-chain rewrite caveat is worth keeping. Another small fixture for
verify: seal two receipts, drop the last one, then verify the remaining prefix. I ran that against the snippet and it still returnsTrue(as does an empty list). So this checks internal consistency, not completeness. I'd pass the independently stored expected head into verification and compare the final hash with it; if that anchor can lag, report "verified through receipt N" rather than implying the latest action is covered.