Inspiration
My mother is a petty trader. She wrote everything in a notebook: what sold, how many packs, who paid cash and who took it on credit.
Writing it down was never the problem. Month end was. She would sit with that notebook and a calculator trying to work out whether she had actually made any money, and the money was never in one place. Some of it was cash. Some was still on the shelf as unsold goods. Some was with people who had taken things on credit and never come back. Three piles that had to reconcile, no way to check her own working, and hours gone. At the end of it she still could not tell you with confidence whether the month had been good.
Years later I worked at a grocery store to buy my first laptop, and watched a supervisor do the same reconciliation at the end of every day, then every week, then every month, chasing entries typed twice during rush hour and prices keyed in as text.
Neither of them was bad at the job. Neither had a tool. And no small shop is hiring a data analyst.
Software that answers these questions has existed for years. It just wants your internet and your card, every month. For a shop turning over ₦60,000 on a good day, a twenty dollar subscription is not a small line item. It competes directly with restocking.
That gap is not about capability. It is about access.
What it does
Ledgerit reads a sales file in whatever state the owner keeps it and prints back what it found, like a receipt.
It cleans the file: parses mixed date formats, strips currency symbols out of number columns, removes duplicate rows, normalises the casing variants that appear when different people enter the same value.
It flags what does not add up. Where a recorded total does not equal quantity times unit price, it shows both figures. On our test file that surfaced eight orders, including one billed at ₦10,000 that should have been ₦5,000. It does not correct them. Those are the owner's books, and she needs to see what happened and decide.
Then you can ask it things in plain English. Which day is quietest. What is actually selling. Whether this month beat last month. How customers are paying.
All of it runs on an 8 GB laptop with no internet connection at any point.
How i built it
The governing rule is that pandas computes and the model narrates.
Every figure comes from deterministic Python. The language model never performs arithmetic. It receives numbers that were already calculated and explains what they mean for the business.
That separation exists because we measured what happens without it: a 3 billion parameter model asked to compute over tabular data produced 1 to 2 unsupported figures per 10 answers. In one case it worked out a month over month difference and got it wrong by 10.
Separation alone was not enough. Models still occasionally did arithmetic despite being told not to. So every number in the model's output is extracted and checked against the figures it was given. On a mismatch, generation retries once with a stronger instruction, and if it still fails the unsupported figures are flagged visibly rather than passed on silently. That took unsupported numbers reaching the user to zero across 30 answers and three candidate models.
Stack: SmolLM3-3B quantized to Q3_K_M, served through llama.cpp. BM25 retrieval over row level records, roughly 40 lines with no dependencies, because an embedding model would be a second download and several hundred MB of resident memory on a machine with 8 GB total. pandas for all analysis. A stdlib HTTP server and a plain web interface, because Electron would cost 200 to 400 MB competing with the model against a 7 GB hard ceiling.
Contribution
I designed and built Ledgerit end to end: the deterministic bookkeeping pipeline, local model integration, GGUF quantization and runtime validation, retrieval and analysis logic, hallucination verification, offline web interface, testing, documentation and submission packaging.
Challenges i ran into
Our own quantizations were silently broken. We built Q3_K_M and IQ4_XS
ourselves and neither carried tokenizer.chat_template metadata. Without that
key, runtimes fall back to a template the model was never trained on. No error,
no warning.
The dangerous part was that it failed differently depending on the runtime. In llama.cpp it produced obvious corruption. In Ollama, which the competition FAQ says judges use, it produced fluent output that silently ignored the system prompt: performing arithmetic it was told never to perform, running to five paragraphs when instructed to write three sentences, and on two of three test prompts never terminating at all. One needed manual interruption after 17 minutes.
A judge would not have seen corruption. They would have seen a runaway generation inventing figures, which is exactly the failure the architecture exists to prevent, arriving through a channel it could not defend. i fixed it by rewriting the GGUF metadata header without requantizing, and verified the fix in Ollama specifically rather than only in our own harness.
Question routing took four attempts. Measured at 68.8% on a 48 question set. A two stage rewrite fixed the category we targeted, taking it from 20% to 100%, but collapsed three categories that had been at or near 100%. Net regression of 18 points at four times the latency. I only caught it because we measured the full set rather than the eight cases that prompted the change.
The fix turned out to be naming, not logic. Our category descriptions contained
nouns the model emitted instead of the label key: asked to classify "what are my
best sellers", it replied bestsellers, a word from our own description text.
Removing those collisions took routing to 93.8% and made classification fully
deterministic.
Cold measurement matters more than we expected. Three consecutive runs of the same model file gave 6.13, then 7.11, then 16.23 tokens per second as OS page cache residency accumulated. The audit runs cold, so only the first read is meaningful. We report the most conservative number, not the most favourable.
Accomplishments I'm proud of
The final official profiler run measured 2,040 MB peak RSS against a 7GB budget, with the model, retrieval and analysis all running locally.
Zero unsupported figures reaching the user, measured rather than asserted.
Catching the chat template failure before submission, and verifying the fix in the runtime judges actually use rather than assuming our development environment was representative.
What i learned
Almost every assumption we started with was wrong when measured. Q3_K_M is smaller than Q4_K_M and also slower, because its dequantization scheme costs more CPU work per weight. Phi-4-mini was heavier and slower than SmolLM3 despite a comparable parameter count. Three separate routing accuracy figures we recorded early turned out to be artefacts of small sample sizes and did not survive re-measurement.
Optimising a local model is measurement work before it is engineering work.

Log in or sign up for Devpost to join the conversation.