DEV Community

Cover image for Shoebox: a receipt sorter for freelancers that never uploads a receipt
Hamid Naeem Khan
Hamid Naeem Khan

Posted on AI-assisted

Shoebox: a receipt sorter for freelancers that never uploads a receipt

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 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.csv an 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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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-schema format option constrains the output to my fields, so I always get parseable JSON.
  • Extraction: pypdf for 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 category income in 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. -->

My First User

Prize Categories

Top comments (0)