DEV Community

PARITOSH
PARITOSH

Posted on

LedgerLocal - your receipts never leave your laptop

Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission ๐Ÿ’ธ

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

What I Built

LedgerLocal โ€” a CLI that turns a freelancer's folder of invoices and receipts into a sorted, accountant-ready archive, entirely on their own laptop.

Point it at a folder: it reads each document with a local vision model, extracts vendor / date / tax / currency / total, categorises it against rules you control, files it into a year/quarter/category tree, and exports the CSV your accountant asks for.

Who pays: freelancers and one-person studios with 300 unsorted receipts and a lost weekend every tax season. Plenty of SaaS does this โ€” all of it wants the documents uploaded. Receipts name your clients, expose your rates, and carry card digits and tax IDs. For consultants under NDA or anyone under data-residency rules, uploading isn't a price question, it's a non-starter. Their real choice is local tool vs. the weekend.

The best part isn't extraction, it's distrust: every document is audited after the model answers. In the demo the model misreads $65.09 as $65.00 and the tool catches it from the arithmetic โ€” nobody told it to look.

Demo

Code

GitHub logo Pager-dot / ledgerlocal

Local-first invoice & receipt sorter for freelancers, powered by open-weight models.

LedgerLocal

Sort a year of invoices and receipts into a clean, accountant-ready archive โ€” on your own laptop, with an open-weight model, with no network access at all.

Point it at the folder where your receipts pile up. It reads each one with a local vision model, pulls out vendor, date, tax and total, categorises it against rules you control, files it into a sane folder tree, and exports the CSV your accountant actually wants.

$ ledgerlocal ingest ~/Downloads/receipts
  read    hetzner-feb.pdf
  read    coffee-with-northwind.jpg
  read    adobe-jan.pdf
  read    legal-invoice.pdf

  2026-02-14  Hetzner Online GmbH             32.73 EUR  hosting
  2026-04-03  Blue Fox Coffee                 11.77 EUR  meals
  2026-01-07  Adobe Inc.                      65.00 USD  software      review
  2026-03-02  Bright Path Legal LLP          990.00 USD  professional_fees

4 ingested, 0 already known, 0 failed, 1 need review

$ ledgerlocal review

1 document(s) the pipeline is not confident about:

  2026-01-07  Adobe Inc.                       65.00 USD
      ยท subtotal + tax differs from total
โ€ฆ
Enter fullscreen mode Exit fullscreen mode

How I Built It

  • Open-weight models: Qwen2.5-VL 7B (photos, scans) and Qwen2.5 7B (text-layer PDFs), served locally by Ollama.
  • Two local runtimes: a three-method provider interface with an Ollama client and an OpenAI-compatible one (llama.cpp, vLLM, LM Studio). Swapping models or pointing at a fine-tune is one config line.
  • Schema-constrained decoding is load-bearing โ€” the JSON schema goes to the runtime itself, so the model physically can't emit malformed JSON. Available because I control the decoder.
  • The model never formats anything. It finds the total; Python decides "1.234,56 โ‚ฌ" is Decimal("1234.56"). Small models are good at finding, unreliable at formatting.
  • Audit, then one repair pass. Subtotal + tax must equal total; failures go back to the model with the specific complaint, best answer wins.
  • Zero required dependencies โ€” the whole model client is stdlib urllib against localhost. When the pitch is "audit this yourself," a 40-package tree undermines it.

SQLite keyed on content hash (re-running a folder is a no-op). ~3,600 lines, 89 tests, no GPU or network needed. Apache-2.0: github.com/Pager-dot/ledgerlocal

Why Does Open Innovation Matter?

The headline feature is one a closed API cannot ship. Rather than ask users to trust a privacy policy, I removed the capability. Every command installs a process-wide socket allowlist first:

socket.create_connection(("api.openai.com", 443))  # OfflineViolation
socket.create_connection(("127.0.0.1", 11434))     # fine โ€” that's your model
Enter fullscreen mode Exit fullscreen mode

A hosted version can't implement this, because the egress it blocks is the call the product depends on. Not a cheaper substitute โ€” a product that otherwise can't exist.

Unmetered inference changed the engineering, not just the bill. The repair pass runs on every uncertain document; on a metered API you'd ration that. recategorise replays three years of documents against new rules in under a second with zero model calls โ€” there's a test asserting the provider is never touched. With per-document pricing you simply don't change your mind, and the rule system dies unused.

Swappability is leverage you keep. A 3B on battery, a 32B at year-end, a LoRA fine-tuned on one client's bizarre invoice layout. With a closed API that last one is a feature request to a vendor who'll never prioritise it.

Where closed wins, honestly: a frontier model reads a crumpled thermal receipt better than a 7B. I closed most of that gap with structure instead of scale โ€” constrained decoding, deterministic parsing, arithmetic audits โ€” and surfaced the rest in a review queue. The discipline a small model forced made a better product, because a frontier model would have tempted me to skip the checks.

Top comments (0)