- Role
- Design and build
- Built with
- Firefly III, Python, JSON rules, Local CLI
Bookkeeping across several accounts and more than one currency is not difficult work. It is repetitive work, which is worse, because repetitive work is where mistakes hide and where the will to do it at all runs out. The month you skip is the month that takes four hours to reconstruct.
The tempting fix is to point an automation at the bank and let it fill the ledger. That is also how a set of books gets quietly ruined, because an importer that runs unattended will happily enter the same fortnight twice and leave you unable to tell which half of the ledger to believe.
Three layers, and only one of them writes
The books live in Firefly III, an open source finance manager running on a server you control. It owns the accounts, the categories, and the history, and it enforces the thing a ledger should enforce: every transaction moves money from a named source to a named destination.
In front of it sits a small Python tool that is the only thing permitted to write. Above that sits an agent, which is where the mess is allowed to be. The agent reads receipts, exports pasted out of a banking portal, and one line records written the way a person actually thinks them, then turns them into the strict format the tool accepts. Judgement happens in the layer that can be argued with, and writing happens in the layer that cannot.
Rules as a file, not as code
Categorisation lives in a JSON file next to the program: an ordered list of patterns, each one naming a keyword, a description, a category, and any tags. The first match wins, so the order of the list matters.
Adding a rule means editing a list, not editing a tool, and reading the file tells you exactly how a given entry got its category. The file is in version control, so the way money has been classified over time has a history rather than a current mood. Malformed rule data stops the tool with an error instead of quietly filing something in the wrong place.
Nothing is written without being shown first
Preview is the default and the commit is a separate flag. A preview prints every transaction that would be created, in full, with the rule that produced it, and then stops. A person reads that list and approves it before anything is written.
Before a batch starts, a read only query reports the latest transaction already recorded for each account, so work resumes from a real cutoff rather than a remembered one. Only one account and one currency are processed at a time, and the parser refuses input it cannot be certain about: future dates, zero amounts, missing descriptions, currency mismatches, ambiguous transfers, and duplicate payloads all stop the run rather than producing a best guess.
Written, then read back, then reversible
A commit is not trusted because the request succeeded. Every write is read back from the ledger afterwards, and the run fails if the stored transaction or the resulting account balance cannot be verified. A network timeout is treated as an unknown rather than a failure, and the rule is to inspect the live account instead of retrying, because a retry is how one payment becomes two.
Every committed batch is journalled, so the tool can be asked to undo the last one. Rollback follows the same shape as everything else: it first prints the exact entries it would delete and waits for approval, and the journal is only updated once a deletion is confirmed by the ledger itself.
Duplicate detection is left switched off, which looks wrong and is not. Two identical amounts to the same shop on the same day happen all the time, and a tool that silently drops the second one is worse than one that shows you both and asks.
Want something like this?
Tell us what the finished thing should do and roughly when you need it. You get a straight answer about scope and price.