Inspiration

Everyone drafts with AI now, and the failure mode is always the same: fluent, confident sentences with nothing behind them. The sentence that's wrong looks exactly like the sentence that's right.

Every tool I found responded to this by writing more text — rewriting your claim, softening it, suggesting a citation. That's backwards. If I already wrote the sentence and I already have the sources, I don't need prose. I need a verdict.

So I set one hard constraint and refused to break it: Receipts never writes, rewrites, or suggests text. It only adjudicates.

What it does

You paste a draft and upload the sources you based it on. Receipts breaks the draft into atomic factual claims and marks each one:

  • SUPPORTED — the sources state or entail it
  • CONTRADICTED — the sources state something incompatible
  • UNSUPPORTED — the sources are silent, partial, or tangential

Each claim is highlighted in place, so your draft still reads as prose rather than a list of findings. Hover, tap, or Tab to any highlight and you get the verdict, the one-line reason, and the exact passage from the source that settles it.

Two rules do most of the work:

It judges against your sources only. A claim that's true in the world but absent from your documents is UNSUPPORTED, not SUPPORTED. Background knowledge is explicitly excluded.

Every citation is verified to exist. A model asked for a quote will sometimes produce a fluent, plausible, nonexistent one. Before any verdict is shown, the quote is checked as a literal substring — whitespace normalised, nothing else — against the source the model named. If it isn't there, the claim is downgraded to UNSUPPORTED and flagged. That guard is the whole trust story.

How I built it

Next.js (App Router) + TypeScript in strict mode + Tailwind, one API route, deployed on Vercel. Google Gemini on the free tier, behind a swappable provider interface so no pipeline code knows which model it's talking to.

The pipeline is three steps: decompose → verify → guard.

Three decisions shaped everything:

Never ask the model for character offsets. Models can't count characters. Instead the model returns each claim's span copied verbatim from the draft, and the offsets are located in code with indexOf. A span that isn't found verbatim is dropped rather than approximated — so highlights are always exactly right, or absent.

Each claim is stored twice. span is the untouched substring used for highlighting; claim is a self-contained restatement with pronouns resolved, used for verification. "It employs 4,000 people" highlights as written but gets verified as "Acme employs 4,000 people."

No retrieval, on purpose. With one to three documents, every source goes into context in full. At this scale that's more accurate than chunking and embedding, and it deletes an entire subsystem. Embeddings are the honest answer at 500 documents — that's in Future Work, not in the build.

Verification batches every claim into a single model call. One request per claim would exhaust the free-tier rate limit partway through a long document.

I wrote the guard's unit tests before the guard: exact match passes, whitespace-variant passes, paraphrase rejected, fabrication rejected, and a real quote attributed to the wrong source rejected. 18 tests, all green.

Challenges I ran into

The model ID I "knew" was dead. gemini-2.5-flash returns 404 — no longer available to new users. Every string I'd have written from memory fails on a freshly issued key. I had to query the live model list and time the candidates. Same story with the API shape: the current surface has no temperature field at all, so determinism is pinned with a fixed decoding seed instead — which is better for reproducible demos anyway.

A <button> destroyed the reading experience. My first annotated draft rendered one claim per line instead of prose. Chromium treats form controls as atomic inline boxes that refuse to break across lines, regardless of display. Switching to <mark role="button"> fixed it — and <mark> is the semantically correct element for a highlight.

Unlayered CSS silently beat Tailwind. My .glass-edge { position: relative } outranked Tailwind's .absolute, because plain CSS wins over anything inside an @layer. The evidence popover was quietly sitting in the text flow and shoving the draft apart. Moving my classes into @layer components fixed it; I verified it by measuring that the prose height is identical whether a card is open or closed.

Free-tier quota is per-model and project-scoped. I exhausted one model, moved to the next, exhausted that too. Rotating to a brand-new API key didn't help — the budget follows the project, not the key. Switching GEMINI_MODEL is the real workaround, and it's now documented.

The guard was invisible. On the demo fixtures the model cites accurately, so the guard correctly finds nothing — meaning the mechanism the whole project rests on could never be seen. I added a labelled switch that swaps one real citation for a fabricated one after verification and before the guard, so the guard has no idea it's synthetic and catches it like any other.

Accomplishments I'm proud of

  • The guard demonstrably catches a fabricated citation, and you can watch it happen in the UI
  • Every highlighted span maps to the exact characters in the draft — verified programmatically, not by eye
  • Zero axe-core violations across WCAG 2.1 A and AA on both screens. Every claim is keyboard-reachable, verdicts never rely on colour alone (each has a glyph and a distinct underline style), and all ambient motion disables under prefers-reduced-motion
  • A dark "aurora glass" interface with real 3D depth that never costs legibility — ambient colour stays behind the content, and every panel carrying body copy is an opaque plate

What I learned

Check the docs, never your memory, for anything provider-shaped — model IDs and API surfaces move faster than training data. Build the falsifier before the feature: writing the guard's tests first meant I knew it worked before I trusted a single verdict. And an "unsupported" verdict is not "false" — keeping that distinction visible in the UI was a design problem, not a modelling one.

What's next for Receipts

  • Embedding-based retrieval for large source sets — the honest answer at 500 documents
  • PDF and DOCX ingestion, so sources don't have to be converted to .txt
  • Per-claim recheck after editing a source, without a full pass
  • An exportable annotated report with citations, for handing to an editor
  • Multi-span claims supported jointly by passages in two different sources

Built With

Share this project:

Updates

Submission history