DEV Community

Gabriel Almeida
Gabriel Almeida

Posted on

Local Receipts: a receipt organizer for freelancers, powered by local Gemma 3

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

Local Receipts is a receipt organizer for Brazilian freelancers and MEIs (micro-entrepreneurs). You give it the text or a photo of a receipt or invoice, and it returns the vendor, tax ID (CNPJ), date, total and expense category. At the end of the month you get a summary by category and a CSV that opens straight in Brazilian Excel.

The person who would pay for it is the freelancer who spends the last weekend of every month digging through receipts for their accountant. Those receipts hold client names, tax IDs and amounts, which is exactly what you don't want to upload to someone else's server.

Demo

Real output from my machine, with the model running locally:

curl -X POST localhost:3000/receipts -H 'content-type: application/json' \
  -d '{"text":"PADARIA PAO QUENTE LTDA\nCNPJ: 11.222.333/0001-81\nEmissao: 03/10/2026\nTOTAL R$ 21,50"}'
Enter fullscreen mode Exit fullscreen mode
{
  "id": 1,
  "vendor": "PADARIA PAO QUENTE LTDA",
  "cnpj": "11222333000181",
  "cnpjValid": true,
  "issuedOn": "2026-10-03",
  "totalCents": 2150,
  "currency": "BRL",
  "category": "alimentacao",
  "needsReview": false
}
Enter fullscreen mode Exit fullscreen mode

A month of receipts exports to a CSV with ; separators, decimal commas and a UTF-8 BOM, so it opens correctly in Excel without any import wizard:

data;fornecedor;cnpj;valor;moeda;categoria;revisar
2026-10-03;PADARIA PAO QUENTE LTDA;11222333000181;21,50;BRL;alimentacao;não
2026-10-08;Notion Labs Inc.;;10,00;USD;software_e_assinaturas;sim
2026-10-10;POSTO ESTRELA COMBUSTIVEIS;44555666000100;198,40;BRL;transporte;sim
Enter fullscreen mode Exit fullscreen mode

The last two rows are flagged for review on purpose: the Notion invoice is in dollars, and the gas station's tax ID fails validation (I made it up for the test).

Code

🧾 Local Receipts

Organize os recibos do seu MEI ou freela sem mandar um centavo (nem um CNPJ) para a nuvem.

Você envia o texto ou a foto de um recibo ou nota fiscal. Um modelo open-weight rodando na sua máquina (Gemma 3 4B via Ollama) lê o documento e extrai fornecedor, CNPJ, data, valor e categoria. O código confere o CNPJ (dígitos verificadores) e marca para revisão tudo que não consegue confirmar. No fim do mês, você tem o resumo por categoria e um CSV que abre direto no Excel brasileiro.

Feito para o Hacktoberfest 2026 Open-Source AI Challenge, tema Side Hustle.

Por que não confiar só no modelo

Um modelo de 4B parâmetros lê bem, mas erra. Por isso o modelo só sugere e o código decide:

  • CNPJ: validado com os dígitos verificadores. Inválido ou ausente → needsReview.
  • Moeda: o valor nunca é convertido. Documento…

How I Built It

The open-weight model. Gemma 3 4B, served by Ollama on my own machine. It reads both text and images, so one model handles pasted text and photos of receipts. Ollama's format field takes a JSON schema (generated from a zod schema), so the model has to answer in a fixed shape. The reply is validated with the same schema, and an invalid one becomes a clean 503 instead of garbage in the database.

The model suggests, the code decides. A 4B model reads well but is wrong sometimes, and it is bad news when it is wrong about money. So everything checkable is checked outside the model:

  • The CNPJ is validated with its two check digits. If it is missing or invalid, the receipt is flagged.
  • Amounts are stored as integer cents.
  • The same document is never entered twice (content hash).
  • If the date, tax ID or total can't be confirmed, the receipt gets needsReview: true, and the model is told to answer null rather than guess.

Two bugs my own tests found. Mocked tests passed, so I ran real receipts through Gemma and found two problems:

  1. A gas station receipt came back as food (alimentacao). Fix: I added a short category guide to the prompt, and it now comes back as transporte.
  2. A US$ 10.00 invoice was saved as R$ 10,00 with no warning. That is the kind of silent error that costs real money. Fix: the model now extracts the currency, the amount is never converted, and any non-BRL document goes to review and stays out of the BRL total.

Both are the kind of failure a happy-path demo never shows. The final suite has 9 tests with the model mocked, and the real model was run against the cases above.

Stack: Node, TypeScript, Fastify, zod, SQLite (better-sqlite3), Ollama, Gemma 3 4B.

Why Does Open Innovation Matter?

  • Privacy is the product. A receipt has a CNPJ, an amount and sometimes a client's name. With a local model, none of it leaves the laptop, and that is a feature I can honestly put on the box.
  • The unit economics work. Processing 10 or 1,000 receipts costs the same: zero marginal cost. There is no per-token bill that forces a subscription price up.
  • It works offline.
  • No lock-in. OLLAMA_MODEL swaps the model, and open weights mean a model could later be fine-tuned on Brazilian invoices, which a generic API handles unevenly.

I didn't compare against a closed API, so I won't claim it would extract worse. The point is where the data goes and what each document costs.

Limitations

  • My photo test used a clean image I generated myself. A crooked, shadowed phone photo will fail more often, and I haven't measured by how much.
  • No direct PDF support yet (convert to an image or send the text).
  • Categories are the model's suggestion, so review what comes flagged.
  • No login: it is designed as a single-user local tool.

What's Next

A tiny upload page, PDF support, and fine-tuning on Brazilian invoice formats.

Prize Categories

Best Use of Gemma: Gemma 3 4B runs locally through Ollama and does all the extraction, for both text and photos.

Top comments (0)