cd /news/ai-tools/i-built-a-receipt-scanner-for-myself… · home › topics › ai-tools › article
[ARTICLE · art-151487] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

I Built a Receipt Scanner for Myself: 15 Bills, Zero Wrong Numbers

A developer built LedgerLens, an open-source Node.js pipeline that turns a folder of receipt photos into an accounting CSV while running Tesseract OCR as WebAssembly locally so no image is ever uploaded. The tool extracts vendor, date, invoice number, line items and total from each receipt, then flags duplicates, price jumps, outliers and arithmetic errors; the author reports a first parser that grabbed the last line containing "Total" returned 459.00 on a bill that read 4600.00, leading to a design rule that unreadable fields return null rather than a guess.

by read7 min views1 publishedOct 16, 2026

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

LedgerLens turns a folder of receipt photos into an accounting CSV, then finds the bills you already paid.

Point it at a folder of .jpg/.png receipts. It runs OCR on your own machine, extracts the vendor, date, invoice number, line items and total from each one, writes one clean row per receipt to data/ledger.csv, and flags the things a human skimming a spreadsheet misses: a bill entered twice, a price that jumped, a total far outside what that shop normally charges, or items that do not add up.

It is for the freelancer or small shop owner who keeps a shoebox of receipt photos and no useful record of them. The information is all there, nobody has time to type it in, and the cloud services that would do it charge per page and upload every receipt to a server you do not control.

That last part is the whole point. A shop's receipts carry client names, GST numbers, addresses and staff details. LedgerLens never uploads an image anywhere. Tesseract runs as WebAssembly inside Node, so there is no Python, no native toolchain, no API key, and no per-image bill. After a first run that downloads about 11 MB of English language data, it works with the network switched off.

$ npm run pipeline

         78.0 conf          0.5s  receipt-01.png
         82.0 conf          0.2s  receipt-02.png
         78.0 conf          0.4s  receipt-03-duplicate.png
         76.0 conf          0.3s  receipt-04-stationery.png

4 image(s) -> data/raw
    ANNAMAIYA PROVISION STORES       2026-10-12    1451.34  ok  via total
    ANNAMAIYA PROVISION STORES       2026-10-14     575.52  ok  via total
    ANNAMAIYA PROVISION STORES       2026-10-12    1451.34  ok  via total
    SARASWATHI STATIONERY            2026-10-15     649.64  ok  via total

4 receipt(s), 4 with a resolved total -> data/ledger.csv
  receipts : 4
  findings : 1

  [HIGH] duplicate_invoice
      Invoice 2291 from ANNAMAIYA PROVISION STORES appears 2 times at an
      identical 1451.34. Likely a double scan or a double payment.

Those are the synthetic samples the repo ships. On real photographs it reads the paper and hands back one row per bill.

Photograph a receipt. Get an accounting CSV. Find the bill you already paid.

Runs entirely on your own machine. Tesseract OCR executes as WebAssembly inside Node, so there is no Python, no native toolchain, no API key, and no image is ever uploaded anywhere. After the first run — which downloads about 11 MB of English language data — it works with the network off.

Built for the Hacktoberfest Open-Source AI Challenge, Week 2: Side Hustle.

A small business or freelancer keeps a folder of receipt photos and no useful record of them. The information is all there; nobody has time to type it in Cloud OCR services do the reading and bill per page, which for a shop doing hundreds of small purchases is a real recurring cost — and they upload every receipt to a server the owner does not control.

Financial…

Plain Node.js, one dependency (tesseract.js), no build step. The pipeline is four small scripts:

script what it does
ocr.mjs images to text, with rotation recovery
parse.mjs text to one CSV row per receipt, plus a self-check
analyze.mjs flags duplicates, price jumps, outliers, bad arithmetic
make-fixtures.mjs redacts real OCR output into safe test fixtures

The design rule everything else follows from: if a field cannot be read confidently, it returns null, never a guess. In a ledger, a blank cell is a nuisance; a confidently wrong figure is a false expense that gets filed and never questioned again.

My first parser took the last line containing "Total". On one real bill, three lines contained the word. It returned 459.00 where the bill said 4600.00 — with no warning, no low confidence, nothing.

I did not trust my memory of how bad that was, so I rebuilt the original logic and ran it against every real bill I had ground truth for. It was worse than I expected:

bill true total original logic shipped
railway ticket 180 (nothing) 180.00
bakery 593 102.00 (wrong) 593.00
bakery (other photo) 593 102.00 (wrong) 593.00
grocery 88 88.00 88.00
credit sale 4600 459.00 (wrong) 4600.00

Original: 1 right, 3 wrong, 1 abstained. Shipped: 5 right, 0 wrong.

The fix was not a better regex. It was label priority: prefer a NetAmount or balance line, never take a tender or "savings" figure, and when two candidates disagree, abstain. Measured, not claimed.

Two suites, reported separately and never averaged together:

synthetic real
cases 7 8 photos, 4 distinct bills
ground truth fixed when written read off the paper by eye
totals correct 7 / 7 7 / 8
wrong 0 0
wrong-rate of produced totals 0% 0%

14 of 15 documents, and no wrong number anywhere. The single miss is the system working: on one photograph it could not read the total cleanly, so it reported nothing. Coverage is the secondary metric; a wrong number is the only real failure.

The same rule caught a GSTIN that OCR had turned from 33ZZQPX1234F1Z2 into 332ZQPX1234F122 — a shape that is no longer a valid GSTIN. The parser returned null and left it for a human, instead of filing a mangled tax number on a real invoice.

The suite is covered by 68 checks across three files (30/30, 12/12, 26/26), plus a 15-check privacy lint wired into CI.

Halfway through, I checked whether the real receipts had leaked into the public repo. The photos had not — samples/real/ was gitignored from day one. But the redacted fixtures derived from them were incomplete: real shop names, a street address, a pincode and bill numbers were sitting in fixtures, a benchmark file, and even the CI workflow. Worse, the test suite depended on a gitignored file, so the privacy guard in CI had never actually run — it crashed before reaching the check.

I fixed it properly: rewrote the redaction rules, added a fixed-point lint that fails the build if a fixture is not idempotently redacted, made the tests hermetic so the guard runs on a clean clone, and purged the leaked content from git history. The public repo now contains only fictional vendors. The gate reports 15/15.

I am including this because a tool whose whole pitch is "your data stays yours" has to hold itself to that, and mine did not at first.

For this project, open isn't a preference. It is the reason the thing works at all.

A closed receipt-OCR API bills per image. For a shop doing hundreds of small purchases a month, that is a recurring cost that eats the margin it is supposed to protect — and every receipt, each one a page of someone's financial life, gets uploaded to a server they do not own. Running the model locally removes both the metered cost and the data exposure in one move. That is the entire product.

It also means I can trust it. The extraction rules and the five detectors are plain code I can read, test and fix. When the "Total" bug turned up, I could rebuild the old logic and measure it against real bills the same afternoon — no black-box API to wait on, no behaviour changing under me. Everything runs offline, on CPU, with no key and no account. That combination — no metered cost, no third-party exposure, and rules you can audit — is what open innovation made possible here. I do not think the closed version of LedgerLens is worth buying, and I do not think that is a coincidence.

I built this end to end with an AI coding agent, including the measurement above and the privacy remediation. The session below is the slice where I stopped trusting a claim, rebuilt the original parser, and found the bug was worse than I had said.

I'm entering:

── more in #ai-tools 4 stories · sorted by recency
── more on @ledgerlens 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-built-a-receipt-sc…] indexed:0 read:7min 2026-10-16 · —