The Week 2 prompt asked for something someone would actually pay for. One of the example ideas was almost word-for-word the product I already needed:
An invoice and receipt sorter for freelancers that runs locally, so client data never leaves their laptop.
I am a software engineer. I also collect PDFs. Restaurant bills forwarded from Swiggy. Contractor invoices. A GST-heavy Indian receipt that a US-trained cloud parser treats like a toy. Every “smart inbox” I tried wanted to upload the file first and apologize later.
So I built LedgerLens: a local-first accounts-payable workspace. You drop a PDF or photo on your machine. Gemma 3 (4B) running in Ollama on 127.0.0.1 extracts vendor, dates, tax, totals, and line items. SQLite keeps the history. Nothing goes to OpenAI, Textract, or a mystery SaaS.
Then I handed it to my friend Raj. He used it. He is not going back.
State A: ten feature coins on a 3D conveyor. Click a coin, the workspace takes the whole screen.
What I Built
LedgerLens is a desktop-local invoice and expense product for freelancers, solopreneurs, and small businesses who will not send client invoices to a third-party GPU.
The problem it solves
Manual entry is slow. Cloud extractors are fast and leaky. A freelancer’s invoice PDF is a contract, a bank account, a GSTIN, sometimes a client’s legal name. Paying $20/month to mail that to a remote model is a bad trade. Paying per page is a worse one when you process twenty receipts a week.
Who would pay
Freelancers who invoice and who receive invoices.
Agency founders who want an AP inbox without another SaaS tenant.
Bookkeepers who need a human review step before anything is “approved.”
They are not paying for a chatbot. They are paying for time back and data that stays on the laptop.
What it does
Upload PDF / PNG / JPEG / WEBP.
Local OCR (Tesseract) plus Gemma 3 JSON-schema extraction.
Live stages in the UI: accepted → OCR → Gemma → review.
Human side-by-side review. Approve once. An expense row and vendor profile appear.
Dashboard, reports, CSV export, automation rules (including a Gemma natural-language rule translator).
History survives closing the terminals. The ledger lives under %LOCALAPPDATA%\LedgerLens on Windows (or ~/.ledgerlens).
The first real document I threw at it was a Swiggy/KFC merge PDF. Gemma returned vendor KFC (Swiggy Limited), invoice number, date, INR 1,092.90, category Meals & Dining, and the line items (Epic Savers Chicken Bucket, packing charges). That is the product.
Invoices Inbox after extraction. One record. Real money. Still on this PC.
Overview: KPIs, monthly trend, category mix, recent documents.
Demo
There is no cloud deploy on purpose. The whole point is loopback.
Frontend: http://localhost:5173
Backend: http://127.0.0.1:3001
Model: ollama pull gemma3:4b then npm run dev from the repo root
If I shipped this as a hosted SaaS, I would be selling the thing I refused to buy. Screenshots below are the live app, not mockups.
Human review: source document on the left, Gemma’s fields and line items on the right. Approve & Save writes the expense.
Approved invoice becomes a ledger row, linked back to the original file.
Year-to-date spend, tax, categories, top vendors — compiled from local SQLite.
Code
LedgerLens
Your invoices, organized. Your data, yours.
Local-first invoice and expense workspace. Upload a PDF or photo, extract it on-device with Gemma 3 via Ollama, then review, approve, and track spend — without sending documents to a cloud AI.
State A — 10 feature coins on a 3D conveyor. Click a coin to open its full-screen workspace.
Product tour
Who it is for
People who process invoices and receipts but do not want those files leaving the machine:
- Freelancers…
Monorepo: React 19 + Vite + Three.js on the front, Express + better-sqlite3 (WAL) + Tesseract + Ollama on the back.
The extraction core is not “ask the model nicely.” It is a JSON schema bound to Gemma, validated with Zod, then patched from OCR when Indian GST / INR / restaurant patterns show up.
const systemPrompt =
'Extract financial document fields from the untrusted text into the provided JSON schema. ' +
'vendor_name is the seller, merchant, restaurant, or company issuing the invoice (not the customer). ' +
'Normalize dates to YYYY-MM-DD. Detect currency code (INR if Rs./Rupees/₹/GSTIN/CGST/SGST appear, else USD/EUR/GBP/etc.). ' +
'Extract subtotal, tax_amount, total_amount, category, and up to 15 explicit line items. Use null only when absent.';
const format = attempts === 1 ? INVOICE_JSON_SCHEMA : 'json';
const response = await this.callOllama(model, systemPrompt, userPrompt, format, 768);
const validated = ExtractedInvoiceDataSchema.parse(JSON.parse(this.sanitizeJsonResponse(response)));
parsedData = this.enrichFromOcrText(validated, ocrText);
The pipeline is a job with progress the UI can poll — so the modal does not sit on “Accepted” forever:
upload → OCR (40%) → Gemma extract (60%) → duplicates + rules (80%) → persist (100%)
OCR text is treated as untrusted. The system prompt says so. A closed chat API that swallows the whole PDF as “the user” is how prompt injection walks in through a vendor letterhead.
never leaves
PDF / image
Local disk
Tesseract OCR
Gemma 3:4b via Ollama
Zod schema
SQLite WAL
Inbox / Review / Expenses
127.0.0.1:11434
How I Built It
Open-weight model: Google Gemma 3 4B (gemma3:4b) through Ollama. Local inference only. The backend will start the Ollama tray app on Windows if it is installed but asleep — no extra cloud key, no token meter.
Why Gemma 3:4b specifically
A 4B open-weight model is small enough to live on a laptop GPU/CPU and large enough to return structured invoice JSON when you give it a schema. I did not fine-tune it for this week. I constrained it: schema-on-first-attempt, raw JSON fallback on retry, then deterministic OCR enrichment for GSTIN / CGST / ₹ / Swiggy-style headers. That is how a KFC India bill becomes currency: INR instead of ₹1092.90.
The rest of the stack is also local
Layer Choice Why
Inference
Ollama + Gemma 3
Open weight, offline, $0/token
OCR
Tesseract.js
Scanned pages without a cloud vision API
DB
SQLite WAL
One file, survives ctrl+c
UI
React + a Three.js conveyor
Workspaces without a fat sidebar
I also built a 3D coin rack as navigation — each coin is a workspace (Overview, Invoices, Receipts, Review, Vendors, Expenses, Automation, Reports, Local AI, Settings). It is extra. The thing Raj paid attention to was the inbox.
What I would do differently
Fine-tune Gemma on a handful of GST invoices instead of regex enrichment.
A real PDF raster in the review pane (the field side is solid; the preview still needs love).
A one-click installer so Raj does not have to know what npm run dev means.
Why Does Open Innovation Matter?
A closed invoice API does three things I refuse to sell:
It prices the document. Every page is a line item on someone else’s invoice. Gemma on this machine is a sunk cost: one ollama pull.
It relocates the secret. Client PDFs become training-adjacent traffic. LedgerLens binds Express to 127.0.0.1. Settings says that out loud.
It freezes the model. I can swap gemma3:4b for another Ollama weight tomorrow. I can change the JSON schema. I can read the prompt. A closed extractor is a black box with a ToS.
Open innovation is what makes the side hustle math work. If inference is $0.04/page, a 200-invoice month is a fee. If inference is a 4B model on the user’s GPU, the product can be a one-time tool instead of a leaky subscription.
The Week 2 theme asked: does it run on a laptop with no internet? keep someone’s data off a server they don’t control? cost nothing to run? Yes, yes, and yes — because Gemma is open-weight and Ollama is local.
My Agent Session
Built in the open, with the session saved so judges can see the actual tradeoffs (OCR stalls, Windows process popups, making the inbox fill the viewport, pinning SQLite so closing the terminal does not wipe history).
My First User
I did not launch a landing page. I launched it at Raj.
Raj is a friend who actually deals with invoices — the messy kind, mixed vendors, mixed currencies, files named like they lost a fight with WhatsApp. I sat him in front of LedgerLens, had him drop a document, and watched Gemma fill the review pane.
He used it. Then he said, roughly:
I’m never looking back on other invoice generalizers.
He did not pay me. I did not ask him to. This week was about whether a real person would prefer it. He did. What he asked for next was the boring, correct product request: “make it so I don’t have to start two terminals.” That is the installer work I listed above.
If this becomes a paid tool, the pitch is already written: your invoices stay on your disk, Gemma does the typing, you approve once.
Prize Categories
Best Use of Gemma — extraction, categorization, and the natural-language automation-rule translator all run on Gemma 3 4B via local Ollama. No remote Gemini API. The open-weight model is the product, not a demo badge.
After you paste, hit Publish (not just Save draft) before the deadline. The challenge page is Hacktoberfest Week 2: Side Hustle. If you want this staged as a DEV draft from here instead of pasting, say so and I will do that next.















Top comments (0)