THE PROBLEM
Hundreds of millions of people now talk to an assistant that remembers them. ChatGPT, Claude, Gemini, every companion app — all shipped a memory feature in the last two years, and all of them keep the same power on the same side of the table. The assistant decides what to store about you. You cannot see why it answered the way it did. And when it holds something you regret telling it — a diagnosis, an income figure, a thing you said in a bad week — your only control is a switch that erases everything or nothing.
This is not a niche developer concern. It is a consumer-protection gap the size of the entire consumer-AI market, and it lands hardest on the people for whom a recorded health fact, belief, or figure carries real cost. The memory field in 2026 — Mem0, Zep, Letta and the rest — is a race to make agents remember better. Nobody is building the other half: a memory that is answerable to the person it describes.
THE SOLUTION
Paper Trail is that half. It is a personal memory file an assistant can read — but the person it is about holds it, and three things are true that are true of no shipped assistant memory:
Nothing is read without a receipt. Every time a value is handed to the model, a row is written to a ledger in the same database transaction as the read. Not the model's account of what it used — the actual handover, logged by the code that does the handing. You watch the ledger fill up as you talk.
You can remove one memory without wiping the rest. Pull a single fact off an answer and the question is re-answered without it, in one click. Revocation you can see working, not a settings toggle you have to trust.
The sensitive stuff is treated differently, structurally. A health fact or a belief is a "special category": it can never ride along inside another request, never travel on a share link, and is stamped separately when you do grant it. A figure you'd rather not disclose can be marked "attested" — the assistant is told it is confirmed without ever seeing the number.
And the governance follows you: mount the same file in Claude over MCP and an external agent gets the exact same rules — ask, receipt, grant, revoke — because there is one policy engine, not one per surface.
WHO THIS HELPS
The guarantee is the same for everyone, and it matters most to the people with the most to lose from a leak:
- Someone with a chronic condition can let an assistant help plan meals or workouts while the diagnosis itself stays a protected category — refused by default, and logged the moment it is used.
- Anyone handing off a slice of their file — to a professional, a service, a friend — gives a link over a chosen subset that expires in an hour, counts every open, and dies on revoke, without exposing the rest.
- Anyone at all gets, for the first time, an answer to "why did it say that?" that is a receipt rather than a reassurance.
The demo makes this legible in sixty seconds to a non-technical person: ask what to cook, pull the "kitchen equipment" memory off, and watch the advice change to assume a pan the persona doesn't own. The abstract promise — you control what it knows — becomes something you see move.
THE CLAIM, AND WHY YOU SHOULD NOT TAKE MY WORD FOR IT
Plenty of projects answer the explainability hole by asking the model which facts it used and printing the reply. That is the model's account of itself: unverifiable, and wrong often enough to matter.
The receipt is written by the code that hands the value over, not claimed by the model afterwards. The model cannot see a memory that was never placed in its prompt, and the stamps under an answer are rendered from rows written in the same database transaction as the read.
That is testable rather than promised, so it is tested:
- test_scope_prompt_never_contains_a_memory_value — the scoping call is handed subjects only
- test_attested_memory_yields_proof_and_never_its_value — an attested salary reaches the model as budget.confirmed: true, never as the number
- test_a_read_that_cannot_be_stamped_does_not_happen — drop the receipts table and the read raises instead of quietly succeeding
That last one is the whole architecture in one test: the read and its receipt are the same transaction, so losing the receipt loses the read.
HOW I BUILT IT
Six pure functions carry the entire model, in policy.py — about 100 lines, no I/O, no state: labels_for_scope (the scoping call sees what a memory is about, never what it holds), validate_request (unknown, revoked, and ride-along special categories all refused), split_by_category (a special category becomes its own request), authorize_share (a link carries live, ordinary memories only), project (the only route a value takes to a model; attested yields proof, never value), and stamps (the row under an answer is built from what was handed over, not what was claimed).
Everything else is deliberately thin. turn.py is the only module that knows the order of the steps, split into propose and answer so there is a moment where nothing has been handed over yet and a person can still say no. store.py is the only module that speaks SQL, and every read and write puts its receipt down in the same transaction as the effect. mcp.py and app.py hold no rules at all — which is why the browser and an external agent get identical guarantees.
Stack: FastAPI, SQLite (stdlib, no ORM), and openai/gpt-oss-20b on Groq at reasoning_effort: low. React 19, Vite, TypeScript under strict; one useReducer, no state library, no router. Answers stream over SSE. One Docker container on loopback behind Caddy, shipped by GitHub Actions over SSH; CI runs ruff, mypy strict, pytest, prettier, tsc, vitest and a production build on every push, and the deploy waits on it. The model was chosen by measurement: gpt-oss-20b returned clean JSON and asked for six memories including the special category, where the larger 120b under-asked — and a scope call that under-asks produces a stamp row that under-reports, exactly the failure this project exists to prevent.
CHALLENGES
Proving a negative. "The model never saw this value" is not something you can demonstrate by looking at output — it had to become a structural property (one function is the only route a value takes to a model) and then a test that asserts the prompt string does not contain it.
Consent people will leave switched on. The first version asked before every read. It was correct and unusable. Standing grants with a TTL, a read count, and a visible revoke button are the compromise: the interruption happens once, the accountability keeps running.
A public demo of a revocation feature, with no accounts. Every row is keyed by session, so your revoke cannot change what the next visitor sees. Sessions are purged after 24 hours, because visitors paste personal text into this and there is nobody to ask for a deletion.
A model allowed to be wrong. The scope call can hallucinate a path or return unparseable JSON. parse_needs intersects with the offered set, validate_request refuses the rest, and if the upstream is unreachable the app serves clearly labelled stand-in answers while every mechanic still runs for real. The governance does not depend on the model behaving.
ACCOMPLISHMENTS
The honesty invariant is enforced by a database transaction, not a prompt. 79 tests (60 Python, 19 TypeScript) written as the specification, one per property. ruff, mypy strict and TypeScript strict all clean, gating the deploy. The whole authorization model is six pure functions you can read in five minutes. And it is genuinely live, no sign-up, no key, working on a phone.
WHAT I LEARNED
Consent is a UI problem before it is a policy problem. And an audit trail the audited component writes about itself is not an audit trail — the only receipts worth anything are the ones produced by the code doing the handing over, in the same transaction as the effect.
WHAT'S NEXT
Conflict detection: a new memory that contradicts an old one should be surfaced rather than appended beside it — the failure mode of every memory system that only ever adds.
Conflict detection: a new memory that contradicts an old one should be surfaced rather than appended beside it — the failure mode of every memory system that only ever adds.
PROVENANCE
Built independently in August 2026, during the hackathon window. The policy and receipt engine is adapted from Agent Visa, my own earlier MIT-licensed project. Everything else was written for Paper Trail. Not built: accounts.
Assistant memory is now a mainstream consumer feature and a one-sided one: the system decides what to keep about a person, that person can't see how it was used, and can't remove one thing without wiping all of it. The harm is not hypothetical — it concentrates on people for whom a recorded health fact, belief, or figure carries real cost.
Paper Trail moves the control to the person the memory is about, and proves it rather than promising it. Every read leaves a receipt written in the same transaction as the read, so the audit trail cannot be falsified even by the model itself. Any single memory is revocable and you can watch an answer change once it's gone. Special categories (health, beliefs) are refused by default and can never leak through a share. The same guarantees hold when the file is mounted into another assistant over MCP, so the protection travels with the person instead of ending at one app's boundary.
It is live, free, requires no account, and works on a phone — so the person with the most to lose does not need to be a developer to use it. The mechanism is small enough to audit: six pure functions, ~100 lines, backed by 79 tests. This is a working demonstration that consumer-AI memory can be accountable to its subject without giving up its usefulness — a template the assistants people already use could adopt.
Log in or sign up for Devpost to join the conversation.