{"slug": "ledgerlens-a-local-gemma-invoice-workspace-my-friend-actually-used", "title": "LedgerLens: A Local Gemma Invoice Workspace My Friend Actually Used", "summary": "A software engineer built LedgerLens, a local-first invoice and expense workspace that runs Gemma 3 (4B) through Ollama on 127.0.0.1 to extract vendor, date, tax, totals and line items from PDFs and photos, with Tesseract handling OCR and SQLite storing history. The React 19/Vite/Three.js frontend and Express/better-sqlite3 backend include a human side-by-side review step before an approved document becomes a ledger row, and the developer reports a friend, Raj, adopted it for real invoice processing. No cloud deployment is offered by design, so client documents never leave the machine.", "body_md": "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:\n\nAn invoice and receipt sorter for freelancers that runs locally, so client data never leaves their laptop.\n\nI 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.\n\nSo 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.\n\nThen I handed it to my friend Raj. He used it. He is not going back.\n\nState A: ten feature coins on a 3D conveyor. Click a coin, the workspace takes the whole screen.\n\nWhat I Built\n\nLedgerLens 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.\n\nThe problem it solves\n\nManual 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.\n\n*Who would pay*\n\nFreelancers who invoice and who receive invoices.\n\nAgency founders who want an AP inbox without another SaaS tenant.\n\nBookkeepers who need a human review step before anything is “approved.”\n\nThey are not paying for a chatbot. They are paying for time back and data that stays on the laptop.\n\n*What it does*\n\n**Upload PDF / PNG / JPEG / WEBP. Local OCR (Tesseract) plus Gemma 3 JSON-schema extraction.\nLive stages in the UI: accepted → OCR → Gemma → review.\nHuman side-by-side review. Approve once. An expense row and vendor profile appear.**\n\nInvoices Inbox after extraction. One record. Real money. Still on this PC.\n\nOverview: KPIs, monthly trend, category mix, recent documents.\n\nDemo\n\nThere is no cloud deploy on purpose. The whole point is loopback.\n\nFrontend: [http://localhost:5173](http://localhost:5173)\n\nBackend: [http://127.0.0.1:3001](http://127.0.0.1:3001)\n\nModel: ollama pull gemma3:4b then npm run dev from the repo root\n\nIf 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.\n\nHuman review: source document on the left, Gemma’s fields and line items on the right. Approve & Save writes the expense.\n\nApproved invoice becomes a ledger row, linked back to the original file.\n\nYear-to-date spend, tax, categories, top vendors — compiled from local SQLite.\n\nCode\n\n**Your invoices, organized. Your data, yours.**\n\nLocal-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.\n\n*State A — 10 feature coins on a 3D conveyor. Click a coin to open its full-screen workspace.*\n\n| Invoices Inbox | Financial Dashboard | \n|---|---|\n| Search, filter, and fill the workspace with extracted invoices. | KPIs, monthly trend, category mix, and recent documents. | \n\n| Human review | Expense ledger | \n|---|---|\n| Side-by-side source + structured fields, line items, and Approve & Save. | Approved invoices land here, linked back to the original file. | \n\n| Tax reports | Privacy & local storage | \n|---|---|\n| Year-to-date spend, VAT, categories, and top vendors. | History stays on this PC — even after you close the terminals. | \n\nPeople who process invoices and receipts but do not want those files leaving the machine:\n\nMonorepo: React 19 + Vite + Three.js on the front, Express + better-sqlite3 (WAL) + Tesseract + Ollama on the back.\n\nThe 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.\n\nconst systemPrompt =\n\n  'Extract financial document fields from the untrusted text into the provided JSON schema. ' +\n\n  'vendor_name is the seller, merchant, restaurant, or company issuing the invoice (not the customer). ' +\n\n  'Normalize dates to YYYY-MM-DD. Detect currency code (INR if Rs./Rupees/₹/GSTIN/CGST/SGST appear, else USD/EUR/GBP/etc.). ' +\n\n  'Extract subtotal, tax_amount, total_amount, category, and up to 15 explicit line items. Use null only when absent.';\n\nconst format = attempts === 1 ? INVOICE_JSON_SCHEMA : 'json';\n\nconst response = await this.callOllama(model, systemPrompt, userPrompt, format, 768);\n\nconst validated = ExtractedInvoiceDataSchema.parse(JSON.parse(this.sanitizeJsonResponse(response)));\n\nparsedData = this.enrichFromOcrText(validated, ocrText);\n\nThe pipeline is a job with progress the UI can poll — so the modal does not sit on “Accepted” forever:\n\nupload → OCR (40%) → Gemma extract (60%) → duplicates + rules (80%) → persist (100%)\n\nOCR 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.\n\nnever leaves\n\nPDF / image\n\nLocal disk\n\nTesseract OCR\n\nGemma 3:4b via Ollama\n\nZod schema\n\nSQLite WAL\n\nInbox / Review / Expenses\n\n127.0.0.1:11434\n\nHow I Built It\n\nOpen-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.\n\nWhy Gemma 3:4b specifically\n\nA 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.\n\nThe rest of the stack is also local\n\nLayer   Choice  Why\n\nInference\n\nOllama + Gemma 3\n\nOpen weight, offline, $0/token\n\nOCR\n\nTesseract.js\n\nScanned pages without a cloud vision API\n\nDB\n\nSQLite WAL\n\nOne file, survives ctrl+c\n\nUI\n\nReact + a Three.js conveyor\n\nWorkspaces without a fat sidebar\n\nI 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.\n\n**What I would do differently**\n\nFine-tune Gemma on a handful of GST invoices instead of regex enrichment.\n\nA real PDF raster in the review pane (the field side is solid; the preview still needs love).\n\nA one-click installer so Raj does not have to know what npm run dev means.\n\nWhy Does Open Innovation Matter?\n\nA closed invoice API does three things I refuse to sell:\n\nIt 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.\n\nIt relocates the secret. Client PDFs become training-adjacent traffic. LedgerLens binds Express to 127.0.0.1. Settings says that out loud.\n\nIt 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.\n\nOpen 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.\n\nThe 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.\n\n**My Agent Session**\n\nBuilt 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).\n\n*My First User*\n\nI did not launch a landing page. I launched it at Raj.\n\nRaj 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.\n\nHe used it. Then he said, roughly:\n\nI’m never looking back on other invoice generalizers.\n\nHe 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.\n\nIf this becomes a paid tool, the pitch is already written: your invoices stay on your disk, Gemma does the typing, you approve once.\n\n*Prize Categories*\n\nBest 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.\n\nAfter 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.", "url": "https://wpnews.pro/news/ledgerlens-a-local-gemma-invoice-workspace-my-friend-actually-used", "canonical_source": "https://dev.to/poppycastle59/ledgerlens-a-local-gemma-invoice-workspace-my-friend-actually-used-7cd", "published_at": "2026-10-17 12:27:53+00:00", "updated_at": "2026-10-17 12:34:33.628430+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-tools", "ai-products", "developer-tools"], "entities": ["LedgerLens", "Gemma 3", "Ollama", "SQLite", "Tesseract", "React", "Express", "Raj"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/ledgerlens-a-local-gemma-invoice-workspace-my-friend-actually-used", "markdown": "https://wpnews.pro/news/ledgerlens-a-local-gemma-invoice-workspace-my-friend-actually-used.md", "text": "https://wpnews.pro/news/ledgerlens-a-local-gemma-invoice-workspace-my-friend-actually-used.txt", "jsonld": "https://wpnews.pro/news/ledgerlens-a-local-gemma-invoice-workspace-my-friend-actually-used.jsonld"}}