Local-first · open-weight · measured

An invoice sorter that keeps your clients’ names off the internet

Drop a folder of invoices and receipts. Ledger reads each one, extracts eleven typed fields with a 4B open-weight model running on your own CPU, shows you which fields to double-check, and exports a CSV your accountant accepts — for $0.00.

A 31B hosted model is also free, and it is more accurate. We measured both, and the table is on this page.

Field accuracy
66.2%
local 4B, CPU only
Schema violations
0
across 20 fixtures
Cost per 20
$0.00
and no upload

Review

Local runtime · gemma3:4b · on this machine
An example of the review table, showing real output from the fixture corpus
DocumentVendorTotalCheck
NW-2026-0431.pdf(text layer)NORTHWIND ANALYTICS781.2092
1P-8842.pdf(OCR)INKWELL PRINT SHOP480.0041
RM-2026-330871.pdf(text layer)TELIA MOBIL46.9288
HC-2026-0219.pdf(OCR)HARLAN & CO LLP3,188.5057

The Check column is a review-priority score out of 100, not a probability. Rows read by OCR are marked, because that changes how much they deserve trust.

The problem

Three to eight client invoices a month, and a wall of receipts

A freelancer ends each month needing those documents as structured rows for an accountant or a tax filing. There are two ways to get there, and one thing that looks like a third.

$50–150

per month

Pay a bookkeeper

For data entry that takes one person a few hours. Cheaper than the alternatives, and it means a stranger handles every client name, rate and bank detail you own.

2–4 hours

per month

Do it yourself

The same hours, every month, forever, spent re-reading documents whose content you already have. This is the option Ledger exists to remove.

$0.00

and free forever

Paste it into a hosted chatbot

Which is not free in the only currency that matters. Invoice text contains client names, project names, rates and account numbers, and that text goes to a service you do not control.

How it works

Four stages, all of them on your machine

  1. 01

    Read the document

    pdf.js pulls the embedded text layer, which is exact and instant. When there isn't one — a phone photo, a flatbed scan — the page is rasterised in the browser and read by tesseract.js, offline. Which route a document took is shown on its row, because it changes how much the row should be trusted.

    No upload directory. The file is read into memory, used, and dropped.

  2. 02

    Extract against a schema, not a prompt

    The Zod schema is converted to JSON Schema and passed to the model as a grammar constraint, at temperature 0. Eleven typed fields in or a validation failure out — never plausible JSON with a string where a number belongs.

    Measured: zero schema violations across twenty fixtures.

  3. 03

    Score what deserves a human

    Confidence is computed, never asked for. A 4B model asked to rate its own confidence produces a number with no calibration behind it, and it is confidently wrong on exactly the fields that cost money. The score blends whether the value appears verbatim in the source, whether it is well formed, and whether subtotal + tax actually equals total.

    Presented as a review-priority score out of 100. It is not a probability.

  4. 04

    Correct, and keep the correction

    Every edit is captured as a labelled example: what the model predicted, what it should have been, and when. That log is the dataset a fine-tune would need, produced as a side effect of ordinary review rather than as a project.

    Downloadable as JSONL. Stored on your disk, or in Atlas if you opt in.

The study

What a 4B model on four CPU cores actually gets right

Twenty synthetic invoices and receipts, hand-authored ground truth, a schema-constrained prompt and temperature: 0. Every number below was measured on the hardware in the footnote and is regenerated from the committed run by npm run results:export. Nothing here is estimated, and the weakest field is shown rather than averaged away.

Local gemma3:4b, CPU only

Headline accuracy and latency for the local 4B model over 20 fixtures
Field accuracy66.2%
Exact-record accuracyevery field on a document correct5.3%
p50 latency per document57.0s
p95 latency per document103.5s
Throughput60.7/hour
Schema violations reaching the table0 across 19 documents
Cost per 20 invoices$0.00

What parameter count buys, on identical hardware

Field accuracy and latency for a 1B and a 4B model on the same CPU
ModelField accuracyp50
gemma3:1b59.4%37.7s
gemma3:4b66.2%57.0s

Four times the parameters bought 6.8 points of accuracy for 1.5× the latency. The 1B model is not merely worse, though: it filled 96.2% of the fields that had a correct answer, against 80.9% for the 4B — it fills in more, and gets more of what it fills in wrong. That is a real failure mode for an unattended pipeline, and it is why extraction here is review-first rather than autopilot.

Per-field accuracy, worst first

Accuracy for each extracted field, with the number of fixtures scored
FieldAccuracy n
Vendor94.7%19
Invoice no.68.4%19
Issue date89.5%19
Due date89.5%19
Currency78.9%19
Subtotal73.7%19
Tax100.0%19
Total84.2%19
Category15.8%19
Billable47.4%19
lineItems58.3%19

Category scores in the teens and it was predicted to: it is the one genuinely open-vocabulary field, and the scorer accepts only what the document permits. A closed category list would raise this number and make the field useless, which is not a trade worth making quietly. Tax is at the top because it is almost always printed explicitly, and getting a field wrong by returning null instead of a number is the single most expensive mistake in this table.

Against a hosted 31B, at the same $0.00

Local 4B compared with a hosted 31B model on field accuracy
ModelField accuracyCost / 20Content leaves machine
gemma3:4b local66.2%$0.00No
gemma-4-31b-it hostednot run$0.00Yes, to a data_collection: "deny" endpoint

Not run. The free OpenRouter tier allows 50 requests per day and the local arms produce the headline numbers without touching that allowance. Two things stay true either way: the local arm makes zero external requests, and so does the hosted arm’s price. And the comparison would not be a clean ablation even if it were run — a hosted Gemma 4 against a local Gemma 3 confounds parameter count with model generation. That gap is disclosed rather than papered over with a number that would invite the wrong conclusion.

Measured on an i7-1165G7, 4 cores / 8 threads, 16 GB RAM, no GPU, Windows 11. Ollama gemma3:4b Q4_K_M. 20 fixtures, one ambiguous fixture excluded from headline figures and scored leniently. Ground truth authored before the first run and never edited to match model output.

Payment prediction

Which invoices to chase first

TabPFN-3.5 forecasts days-to-payment from invoice history, with a q10–q90 interval on every row. It is scored against two naive baselines on the same held-out invoices, because a forecast you cannot compare against this client’s own average is an assertion rather than a result.

TabPFN-3.5

7.91 days

mean absolute error

Client historical mean

8.12 days

the baseline that matters

Global mean

13.48 days

ignore who the client is

Paid invoices in history

84

Distinct clients

8

Features per row

12

TabPFN wins, by 3%. It is a real margin and it is also a narrow one — who pays late is mostly a property of who the client is, so a well-built per-client mean is a genuinely strong baseline rather than a straw man. The model’s contribution is in the residual: invoice size, timing seasonality and the client’s own variance. Its q10–q90 interval contained the true value in 12 of 17 held-out rows, against an 80% nominal target, so the intervals run a little narrow and are reported that way.

Privacy

There is no document store to breach

Most claims about privacy are promises. This one is checkable: uploaded documents are processed in memory and dropped. There is no upload directory, no retention policy and nothing to delete later, because nothing was kept.

Where each kind of data goes, and when
DataDestinationWhen
Invoice image or PDFNowhereHeld in memory for the duration of extraction, never written to disk
Extracted textNowhereGoes to Ollama on your machine and stays there
OCR outputNowheretesseract.js runs in your tab
CorrectionsYour diskLocal by default; Atlas only if you explicitly opt in
TabPFN feature rowPrior Labs (EU)Numbers plus a hashed client id, and only when you press Predict
Sentry spansSentryModel, token counts, latency, stage names. No text, no names, no amounts

How to verify it in about a minute

  1. Pull the model: ollama pull gemma3:4b
  2. Open the upload screen and watch the runtime banner turn local
  3. Open DevTools, Network, and drop one invoice
  4. Every request goes to 127.0.0.1. There is no other host
  5. Disconnect the network and do it again — still works

The hosted path, and its limits

Where an external call is unavoidable, the payload is the smallest viable projection of the data. Redaction before send is best-effort obfuscation, not anonymisation, and this page does not claim otherwise.

  • Client names and account-number-like digit runs stripped on device
  • data_collection: "deny" restricts which endpoints are eligible
  • No compliant endpoint means the request fails, not that it degrades
  • This controls which endpoints qualify, not what every intermediary does. It is meaningfully better than an unconstrained call and categorically different from local.

Pricing

A hypothesis, stated rather than charged

Extraction is a utility anyone can provide, and a local one is free forever. The thing nobody else offers is the chase list, so that is what a paid tier would be framed around. There is no checkout here, and there will not be one.

Local

Unlimited documents, forever

$0

  • Batch extraction on your own hardware
  • Review, correction and CSV/JSON export
  • Per-field review-priority scores
  • Correction log as JSONL
  • Works offline after the model download

Sync + chase list

The hypothesis under test

$12

per month, if it ships

  • Opt-in Atlas sync of extracted fields
  • Chase list with q10–q90 intervals
  • Corrections as a durable cross-device training set
  • No checkout. Vercel Hobby terms are personal and non-commercial.

Questions

Including the awkward ones

Does my invoice data leave my computer?
On the local path, no. Your browser talks to Ollama at 127.0.0.1 directly, so the extraction request never touches a server and there is no server-side component that could log it. Open the network tab during a local extraction and you will see one destination, and it is your own machine. The app still works with the network disconnected once the model is downloaded.
What happens if I have no model installed?
The runtime banner tells you so plainly rather than pretending. You get a pre-computed dataset — real recorded output from a real run, including its mistakes — so you can exercise review, correction and export without setting anything up. It is labelled as demo data and is never described as live inference.
What is the hosted path, exactly?
An explicit, opt-in alternative for machines with no local model: a 31B model via OpenRouter. It requires one confirmation per session. Client names and account-number-like digit runs are stripped on your device first, requests carry provider.data_collection: deny and require_parameters: true, and if no endpoint satisfying both exists the request fails rather than degrading to something that logs. It is not equivalent to local and the banner does not pretend it is.
How accurate is it, honestly?
66.2% of fields correct across twenty fixtures, and 5.3% of documents fully correct. That is not a system you hand a folder to and forget about — it is a system that does the tedious 60% and shows you exactly which 40% to check. The per-field table on this page says which fields those are, and category scores in the teens.
Why a 4B model and not something bigger?
Because this ran on four CPU cores with no GPU and no credit card. 12B is 8.1 GB and materially slower; 27B would have consumed the remaining disk while remaining unusable. The size curve in the study measures what that constraint actually costs, by running 1B and 4B on identical hardware.
What does the payment prediction actually do?
It forecasts days-to-payment per open invoice with a q10–q90 interval, and it is scored against the naive baselines it has to beat: that client's own historical mean, and the global mean. Only numbers and a hashed client identifier are sent to Prior Labs; the raw client name never leaves your device. Below about twenty historical invoices it is weak, and it says so.
Will it cost me anything?
The local path is $0.00 and there is no checkout anywhere in this app. There is a written $12/month hypothesis in the footer — framed around the chase list rather than extraction — which is a question being tested with the first real user, not a thing you can pay. Payment collection is deliberately absent, partly because Vercel's Hobby terms restrict this deployment to personal, non-commercial use.

Twenty fixtures, one click, and a CSV your accountant accepts

No login, no API key, no upload. If you have Ollama it runs live on your hardware; if you do not, the demo set exercises the whole flow.

Open the app