Writing
Upload a document, a model pulls the numbers out, the numbers get saved. That's how most AI document tools work — and not one of them can tell you whether the number it saved is the number on the page. We built a tax pipeline that can't save a number until something other than the extractor has confirmed where it came from. Halfway down this page you can run that check yourself and watch it throw a number away.
The demo is irresistible: drop in a PDF, watch the fields fill themselves, hit save. When the model reads the wage box correctly you just saved twenty minutes. When it reads one digit wrong you saved twenty minutes and filed a wrong number — and nothing in the system can tell you which of those two things happened, because the output was trusted the moment it appeared.
That isn't a model problem. The proposer and the record-keeper are the same step. Any extractor — a model, a regex, a person having a bad afternoon — will sometimes be wrong. The only question that matters is whether the system is built to notice.
Our extraction step is built on Traverse, and Traverse's contract is blunt: an extraction is a proposal, and a proposal has to quote the passage it came from, word for word. Traverse then goes looking for that quote in the text it actually handed the extractor — a prepared, plain rendering of the document, not the original file. If the quote isn't there, because the extractor paraphrased, summarized, or invented, the proposal is dropped instead of saved. Surviving proposals don't even keep their own source positions: Traverse recomputes every offset from the quote it just located, so "where this came from" is never taken on the extractor's word.
Notice what the contract doesn't ask: who proposed. A hallucination-prone model and a hand-written parser go through the identical check, because the check costs the same either way and the failure it catches is the same failure. Today the production extractors in our own pipeline are deterministic parsers, not LLMs— and they get exactly the same distrust. That's the point: the contract didn't have to change when the proposer did.
Traverse is a public package, so you can run the check without our pipeline, our documents, or an API key. Below is a synthetic 1099-INT with exactly one interest figure on it, and an extractor that proposes two values for that field: one it quotes correctly, and one it made up along with a matching quote — the failure mode the whole architecture exists for.
import { extract } from "@kontourai/traverse";
// A synthetic 1099-INT. 412.18 is the only interest figure on the page.
const statement = "FIRST MERIDIAN BANK -- Form 1099-INT (sample)\n"
+ "Box 1 Interest income 412.18\n"
+ "Box 4 Federal income tax withheld 0.00";
// Two proposals for the same field: one quotes the page, one invents a
// figure and a quote to match.
const proposals = [
{ fieldPath: "interestIncome", candidateValue: 412.18, confidence: 0.96, extractor: "demo",
provenance: { excerpt: "Box 1 Interest income 412.18" } },
{ fieldPath: "interestIncome", candidateValue: 1204.60, confidence: 0.93, extractor: "demo",
provenance: { excerpt: "Box 1 Interest income 1,204.60" } },
];
const result = await extract({
content: statement,
contentType: "text",
sourceRef: "first-meridian-1099int.txt",
targetSchema: [{ path: "interestIncome", type: "number" }],
provider: { name: "demo", async extract() { return { proposals, raw: { response: "{}" } }; } },
});
for (const p of result.proposals)
console.log("kept " + p.candidateValue + " verified at " + p.provenance.locator);
for (const w of result.warnings) console.log("dropped " + w);Install the package and run the file. Nothing in the script tells Traverse which proposal to believe — it checks both quotes against the page and keeps the one that is really there.
kept 412.18 verified at chars:46-84 dropped dropped proposal for "interestIncome": excerpt not found in prepared content
The invented $1,204.60 never becomes a fact. It isn't flagged, scored, or filed under low confidence — it does not survive into the output at all, and the reason is on the record. That refusal is the whole mechanism, and it is one npm install away from any pipeline you are already running.
Here is what it does not prove. The check confirms the quoted passage is really in the document. It does not confirm the number is: an extractor that quotes the right line and transcribes the wrong digit still produces a proposal, and this check will pass it. Catching that is a different job, and it belongs to the stage after extraction.
Past extraction, every fact moves through the same three places: extracted → resolved → verified — three separate stores, not a status column someone remembers to update. Extracted facts carry their quote, their position, a confidence, and a reason. Resolution collapses competing candidates for one slot into a single winner by a fixed precedence order — a person's correction beats a corrected form, which beats the original document — and every losing candidate stays on the record as superseded, which is the fourth place. Nothing is overwritten, so nothing has to be taken on faith later.
The part that makes it structural rather than aspirational: the tax math can only read verified facts. Withholding analysis, the return package, year-over-year comparisons — all of it queries the verified store and nothing else. An unverified fact isn't flagged for caution; it is invisible to the engine. The gate swings both ways, too: when a new document makes an old fact ambiguous, the old fact is demoted — pulled back out of the verified store, both candidates preserved — rather than left coasting on a stale approval.
Most facts never need a person. A fact whose quote checked out, with one candidate and high confidence, promotes automatically. That is not the extractor grading its own work: the quote check already ran, and the extractor had no vote in it. Confidence decides only what gets escalated to a human — it can escalate, it can never excuse. The gate holds back what's ambiguous; it doesn't make you read every number.
Both sketches below use invented figures. They are the shapes of catch the gate exists for, not incidents from anyone's return.
A corrected form arrives. Six wage fields extract cleanly from the first copy of a form — each quoting the box it came from, one candidate apiece, promoted. Then a corrected copy of the same form shows up. Under extract-and-trust the new numbers overwrite the old, or worse, sit alongside them and something downstream quietly picks. Here both versions become candidates for the same six fact keys — a fact key being one slot in the return: one place, one number — promotion stops, and nothing reaches the tax math until a person chooses. The choice is recorded, and the original stays visible as superseded rather than being deleted.
Two statements, one slot. Two interest statements land on the same fact key carrying different amounts. The fact is held at the gate with both candidates preserved, the year's status flips to needs-review — disputed, in Surface's vocabulary — and none of those dollars reach the return until the conflict is explicitly resolved. The failure the trusting pattern can't even detect — two numbers, one silently wins — has nowhere to happen here: there is no path where a fact key with two live candidates is readable by the math.
It doesn't check arithmetic. Nothing in it recomputes a return against an oracle — it checks where a number came from, not whether the return built on it is right.
It isn't uniform, either. Some document types run the full quote-checked route through Traverse; others use direct parsers whose source is recorded but whose values are never checked against a quote. Facts from those paths still have to clear resolution and the gate before the math can see them, but "verified" is doing weaker work for them — it means a known parser and a known source, not a checked quote. The record says which is which, which is the only reason that difference is safe to admit.
And it is a reference implementation, not a product: one filing domain, built to prove that the architecture survives contact with real deadlines.
None of this is really about tax. Our day job is a harness for coding agents, and the rule there is the same sentence with a different noun: an agent's claim doesn't count as done until a check the agent didn't write agrees with it. Here: a number doesn't reach the math until something other than the extractor confirms where it came from. Same shape, different work.
That's why the mechanism never asks what the work is. It asks two things only: was "done" written down as something a machine can check, and did the thing being checked have a vote in the check. Filing is a good stress test because "done" has a deadline and a wrong answer has a penalty attached — but a claims file, an intake summary, or a merged pull request has the same shape underneath. So does the failure: a system that reports success in the same breath it does the work.
Because every stage records the same four things — what was proposed, what was chosen, what was rejected, and why — the pipeline's whole trust state comes out as one record you can read end to end: documents, extractions, resolutions, verified and superseded claims, each with its source and status. It's the same architecture this site publishes for its own deliveries —recomputable receipts — pointed at a domain where a wrong number has a filing deadline.
The pipeline itself isn't open source, so what's public is the architecture and the check you just ran, not the records. The pieces it composes are public too: Traverse for quote-verified extraction, Survey for the review record, Surface for the trust state itself.
If you're building on extract-and-trust, the upgrade isn't a better model. It's a gate the model can't talk its way past.