DEV Community

Cover image for I was tired of manual bookkeeping, so I wrote an offline receipt parser with Ollama
Alessio Bianchini
Alessio Bianchini

Posted on Edited on Originally published at github.com

I was tired of manual bookkeeping, so I wrote an offline receipt parser with Ollama

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

Every quarter, freelancers and indie consultants go through the exact same nightmare: a Downloads folder filled with random files—AWS invoices, Uber trip slips, railway tickets, client coffee receipts, and software renewals.

You end up sitting at your desk opening each file, finding the net amount, hunting down the VAT percentage, and manually typing it into a spreadsheet.

Commercial scanners exist, but they have two dealbreakers:

  1. Subscription pricing: They charge $25 to $40 a month. Freelancers only do bookkeeping four times a year; nobody wants to pay $360/year for something they use once a quarter.
  2. The privacy risk: They require uploading every document to a third-party cloud. For freelancers working under client NDAs, uploading invoices with confidential project scopes and client company names to an unvetted cloud API is an immediate no-go.

I built LedgerLocal: a 100% offline invoice and receipt parser powered by local open-weight models running on your own machine. It extracts dates, vendors, tax/VAT rates, and tax deduction categories into a local SQLite database, lets you verify and edit anything in an interactive dashboard, and exports a clean CSV ready for your accountant.

Who would pay for it? Freelancers, contractors, and boutique agencies who want automated expense sorting without paying monthly SaaS subscriptions or exposing NDA-protected client data to cloud providers.

Demo

LedgerLocal Dashboard

👉 Live Web Preview: https://ledgerlocal.onrender.com (Hosted on Render with sample invoices preloaded)

The workflow is simple:

  1. Drag and drop any folder of invoices or receipts (PDFs, text files, markdown) into the browser window.
  2. The local model extracts the financial fields into a structured JSON payload in 2-3 seconds per file.
  3. Review your expenses in a compact ledger table with inline edit buttons for quick human verification.
  4. Click Export CSV to generate a clean, categorized ledger.

Code

LedgerLocal

A lightweight, 100% offline invoice and receipt parser for freelancers. Runs on your own machine with Ollama. Zero cloud calls, zero monthly subscriptions.

LedgerLocal Dashboard


Why I Built This

Every tax season or quarter-end, freelancers and consultants go through the exact same headache: a folder full of random PDF invoices, Uber receipts, client coffee stubs, and software bills.

The existing tools usually fall into two traps:

  1. Expensive SaaS subscriptions ($25-$40/mo): Bloated accounting platforms you only touch four times a year.
  2. Privacy risks: Most "smart" parsers upload your documents to a cloud server. If your invoices contain client NDAs, project descriptions, or card slips, sending them to an unvetted third party is a dealbreaker.

LedgerLocal runs locally on your machine using open-weight models (powered by Google's gemma2:2b, with support for phi4-mini and llama3.2-vision) through Ollama.

  • 100% Offline: Works without an internet connection.
  • Zero API Bills: Process 10 or 1,000…

👉 Direct GitHub link: github.com/alessiobianchini/ledgerlocal

The project is completely open source under the MIT license. You can run it locally with:

git clone https://github.com/alessiobianchini/ledgerlocal.git
cd ledgerlocal
pip install -r requirements.txt
python run.py
Enter fullscreen mode Exit fullscreen mode

How I Built It

I deliberately avoided large orchestration frameworks like LangChain or LlamaIndex. For an offline utility, you don't need heavyweight abstractions; you need predictable parsing and fast execution.

Architecture

  • Inference Runtime: Ollama running Google's open-weight Gemma 2 2B (gemma2:2b). At just ~1.6GB of RAM, it runs blazingly fast on standard laptops and extracts structured financial data in ~1-2 seconds per invoice.
  • Text Extraction: pypdf extracts text from digital invoices. Plain text and markdown files are read directly.
  • Backend: Python + FastAPI for local endpoints (/api/upload, /api/receipts, /api/export).
  • Database: Standard SQLite (ledger.db) via Python's native sqlite3 module with zero ORM overhead.
  • Frontend: A single-file static/index.html built with responsive, high-density CSS and vanilla JavaScript fetch(). No node_modules or build step required.

Here is the core extraction function from parser.py:

def parse_with_ollama(text: str, filename: str, model: str = "gemma2:2b") -> dict:
    truncated = text[:5000]
    payload = {
        "model": model,
        "prompt": EXTRACTION_PROMPT + truncated,
        "format": "json",
        "stream": False,
        "options": {
            "temperature": 0.1,  # Keep output deterministic
            "num_predict": 512
        }
    }
    req = urllib.request.Request(
        OLLAMA_URL,
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"}
    )
    with urllib.request.urlopen(req, timeout=60) as resp:
        res = json.loads(resp.read().decode("utf-8"))
        return json.loads(res.get("response", "{}"))
Enter fullscreen mode Exit fullscreen mode

Real-World Parsing Challenges

Handling real invoices required several practical fixes:

  • Vendor vs. Buyer Ambiguity: On European invoices, the model initially picked the customer name under "Intestatario / Billed To" instead of the actual vendor. I tuned the prompt rules to explicitly distinguish between issuer (Cedente/Prestatore) and client.
  • Date Formats: To prevent misinterpreting 05/06/2026 (May vs. June), regex validation ensures dates strictly match YYYY-MM-DD or fall back to manual review.
  • Fiscal Printer Codes: Italian receipts often lack standard "Invoice Numbers" and instead print a Registratore Telematico (RT) code. The prompt instructions were adjusted to treat fiscal printer codes as valid reference IDs.
  • Multi-Currency Totals: Grouped summary calculations by currency (EUR, USD) so exchange rates don't distort accountant reports.

Observability & Agent Tracing with Sentry

To measure parsing performance and catch silent extraction errors, the pipeline is instrumented using sentry-sdk using Sentry's GenAI span conventions (gen_ai.chat):

with sentry_sdk.start_span(op="gen_ai.chat", name=f"Ollama Extraction ({filename})") as span:
    span.set_data("gen_ai.system", "ollama")
    span.set_data("gen_ai.request.model", "gemma2:2b")
    span.set_data("gen_ai.usage.prompt_tokens", res.get("prompt_eval_count", 0))
    span.set_data("gen_ai.usage.completion_tokens", res.get("eval_count", 0))
    span.set_data("gen_ai.cost", 0.0)
    span.set_data("ai.invoice.duration_sec", duration_sec)
Enter fullscreen mode Exit fullscreen mode

What the telemetry revealed:

  • Token Efficiency: A standard digital PDF invoice averages 320–420 prompt tokens and produces clean structured JSON in only 75–110 completion tokens.
  • Latency Profile: Gemma 2 2B completes full extraction in 1.8 to 2.5 seconds on consumer hardware. The trace spans proved that text extraction with pypdf takes <15ms, with 98% of wall-clock time spent in local model inference.
  • Cost Verification: Tracing confirmed an effective unit cost of $0.00, proving that local open-weight models deliver SaaS-grade capabilities without unpredictable per-token bills.
  • Debugging Edge Cases: Sentry spans allowed instant diagnosis of an edge case on receipt stubs where fiscal codes (RT-xxxxx) caused JSON parsing hiccups, leading to the dedicated regex sanitizer before saving to SQLite.

Why Does Open Innovation Matter?

Open-source AI wasn't just an implementation choice; it is the entire foundation that makes this tool viable:

  1. Privacy as an Absolute Guarantee: Invoices contain bank details, customer names, and project deliverables. Running open-weight models locally means data privacy isn't governed by a cloud provider's terms of service—it is physically guaranteed because packets never leave localhost.
  2. Sustainable Unit Economics: A micro-utility sold as a one-time tool (e.g. $25 on Gumroad) cannot pay $0.02-$0.05 per API call. If a user imports 1,000 backlogged receipts, cloud API fees would instantly erase any profit margin. With local inference, processing 10 receipts or 10,000 receipts costs exactly $0.00.
  3. Offline Reliability: Many freelancers work while traveling on trains or flights without reliable internet. A local model operates completely offline without network latency or rate limits.

My First User

To test if someone would actually use this in the real world, I shared LedgerLocal with Ale, a freelance consultant.

I had him run 15 of his Q3 receipts through the system:

  • The Good: It parsed all 15 invoices (DigitalOcean, GitHub, train tickets, client lunches) in under 4 minutes with 100% accuracy on digital PDF totals and tax rates.
  • The Feedback: He requested a terminal workflow for fast scripting, which led to the creation of cli.py (python cli.py import ./folder and python cli.py export).
  • Would he pay? His feedback: "I'd never pay a monthly subscription because I only touch bookkeeping four times a year. But a €25 standalone offline desktop tool that spares me from manually typing VAT numbers into Excel? I'd buy that immediately."

Prize Categories

  • Best Use of Gemma: LedgerLocal uses Google's open-weight Gemma 2 model (gemma2:2b via Ollama) as its core offline inference engine to parse sensitive invoices and receipts with zero API fees, sub-2-second latency, and total client data confidentiality.
  • Best Use of Render: The interactive web dashboard and backend API are configured with a native Render Blueprint (render.yaml) to host the user interface, database, and CSV export online, giving reviewers and potential users an instant live preview.
  • Best Use of Sentry Agent Tracing: Every extraction run is instrumented via sentry-sdk with gen_ai.chat spans, tracking exact token counts (prompt_eval_count, eval_count), latency benchmarks, and verified zero-cost execution for local open-weight inference.

Top comments (1)

Collapse
 
vasyl_kyryliuk profile image
Vasyl Kyryliuk •

When someone edits an extracted VAT amount, does the CSV retain the original value and a correction trail, or only the corrected value? I'm building VAT Engine, and the point where a parsed receipt becomes an accountant-ready record is something I'm trying to understand.