This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
Every freelancer I know has the same tax-season ritual: a Downloads folder full of Invoice_final(2).pdf, forwarded receipts, and invoices they sent to clients, and a weekend lost to sorting it. Shoebox does the sorting.
Point it at a messy folder. It reads each document with a model running on your own machine, then:
- files a copy into
YYYY/YYYY-MM/category/with a readable name (2026-04-02_GitHub_4.00.pdf) - flags duplicates (the same receipt emailed twice)
- sets aside anything it can't read or doesn't trust, in
_needs-review/ - writes a
ledger.csvan accountant can open, and prints spend by category, with invoices you sent counted as income
It is a dry run by default, and it copies, never moves, because the originals are your client records.
Who would pay: a freelancer or one-person agency once a year at tax time, or a bookkeeper who wants a first pass over a client's shoebox of documents. I have not tested willingness to pay (see "My First User").
Demo
ollama pull llama3.2:3b
shoebox samples/inbox --out out # dry run: writes out/ledger.csv only
shoebox samples/inbox --out out --apply # copies files into out/YYYY/YYYY-MM/category/
samples/make_samples.py generates fictional receipts and invoices (PDF and text) so you can try it without touching real records.
Code
Shoebox
Local-only receipt and invoice sorter for freelancers. Point it at a messy folder; it reads each document with an open-weight model running on your own machine (Ollama, default llama3.2:3b), files copies into YYYY/YYYY-MM/category/, flags duplicates, and writes a ledger.csv for your accountant. Nothing leaves your laptop: the client refuses any non-loopback host.
Use
python3.11 -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]"
ollama pull llama3.2:3b
shoebox ~/Downloads/receipts --out ~/books # dry run, writes ledger only
shoebox ~/Downloads/receipts --out ~/books --apply # copies files (originals untouched)
pytest
Supports text-based PDFs and txt/md/csv/eml/html. Scanned images need OCR, which is not included.
Trust checks
Small models make mistakes, so every result is validated: the extracted total must appear in the document text, dates must be plausible, and anything failing goes to _needs-review/ instead of being filed silently.
Sample data and eval
…How I Built It
-
Model:
llama3.2:3b, an open-weight model, served locally by Ollama. Ollama's JSON-schemaformatoption constrains the output to my fields, so I always get parseable JSON. -
Extraction:
pypdffor text-based PDFs, plain reads for txt/eml/html. No OCR, so scanned images are listed as unreadable rather than guessed at. -
Validation (the part that matters): a small model will sometimes invent numbers, so every result is checked against the source text. The extracted total must literally appear in the document, the date must be plausible, and a failure sends the file to
_needs-review/instead of filing it. "Invoice you sent" is forced to categoryincomein code, not left to the model. - Privacy by construction: the client refuses any host that isn't loopback, so a misconfigured URL fails instead of leaking.
How well does it work? I generated 15 fictional documents with known answers and scored llama3.2:3b (CPU only, an Intel Mac, about 25-28 s per document):
| field | first prompt | fixed prompt |
|---|---|---|
| date | 15/15 | 15/15 |
| total | 15/15 | 15/15 |
| vendor | 13/15 | 12/15 |
| doc type | 12/15 | 12/15 |
| category | 3/15 | 12/15 |
The first run was bad: category was 3/15 because my own prompt mentioned "income" and the 3B model labelled nearly everything income. Removing it from the model's choices took category to 12/15.
What is still wrong: three supplier invoices were read as invoices I sent, two client invoices got my own name as the vendor, and one vendor came back empty. Of the 6 documents with an error, only 1 was flagged for review; the other 5 would be filed silently. My validator catches hallucinated amounts and bad dates, but not a wrong category or the wrong party. So the honest status is: reliable on amounts and dates, shaky on judgment, and you should skim the ledger before trusting it for taxes. This is 15 synthetic documents and a single run, so treat it as indicative, not a benchmark.
Why Does Open Innovation Matter?
Receipts and client invoices are exactly what you don't want to paste into someone else's API: client names, rates, addresses. Running an open-weight model locally means the documents never leave the laptop, there is no per-page fee eating a small business's margin, and it works on a train with no signal. I can also swap the model with --model as better small models appear, and a niche like "invoices from my country's utilities" is something I could fine-tune for, which a closed API wouldn't let me do. The tradeoff is real, though: a 3B model on a CPU is slower and less accurate than a frontier API, and I'd say so to any customer.
My Agent Session
I built this with Claude Code. <!-- Add a DevRelay link here if you saved one, otherwise delete this section. -->
Top comments (0)