Build Monarch Money
KINDAreplaces $14.99/mosaves $179.88/yrback to the verdict
A local ledger built from file imports and, optionally, one aggregation provider: deduplicated transactions, rules-based categorisation you can correct, accounts and balances rolled into net worth, budgets that compare against real spending, and reports that reconcile to the penny.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the importers, reconciliation and reports.
Get it python.org or your package manager, then a virtual environment.
Verify
python3 --version prints 3.12 or higher - have readyfree
Why Deduplication, sign conventions and transfer pairing are all invisible with one account and one month, and they are the parts that make the numbers trustworthy.
Get it Download the export files your bank already offers. Include a credit card, because its sign convention will differ.
Verify
At least two accounts and twelve months are available, including one credit card - have readyfree
Why Reconciliation needs a ground truth. Without asserted balances you cannot know whether your ledger is complete.
Get it From the statements you already receive, one balance per account per month.
Verify
At least three asserted balances per account are recorded - decidenothing for file import; roughly 15 to 40 US dollars a year for a personal aggregation service
Why It determines cost and region and is worth deciding before building rather than after. File import works everywhere and costs nothing.
Get it If you are in the United States, SimpleFIN Bridge at around 15 US dollars a year suits read-only personal finance, and Teller offers a limited free developer tier. In Europe, Enable Banking offers restricted production access for your own accounts. Verify current terms and pricing yourself, because this market changes.
Verify
The decision, the provider, its cost and its data access are written down - decidefree with a drive you own
Why This database is a complete record of your finances. It deserves a better backup story than a file in a synced folder.
Get it An encrypted archive to a destination you control. Decide before you import a year of history.
Verify
An encrypted backup has been created and restored once
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- accounts(id, name, institution, kind, currency, opened_at, closed_at) · kind is current, savings, credit, investment or asset - transactions(id, account_id, posted_on, amount_minor, description, raw_description, external_id, import_id, category_id, transfer_pair_id, notes) - imports(id, account_id, source, file_hash, imported_at, row_count, duplicate_count) - rules(id, priority, match_field, pattern, category_id, enabled, created_from_correction) - categories(id, parent_id, name, kind) · kind is expense, income or transfer - balances(account_id, on_date, balance_minor) · asserted from statements, used to reconcile - budgets(id, category_id, period, amount_minor) - A transfer between your own accounts is not income and not spending. Pairing them is what makes every total believable.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DB_PATH | required | ./data/ledger.db | The ledger. A complete record of your finances, so treat the file and its backups accordingly. |
BASE_CURRENCY | required | GBP | Reporting currency. Multi-currency accounts convert into it for totals. |
TRANSFER_WINDOW_DAYS | required | 4 | How far apart two sides of a transfer may be before they stop being proposed as a pair. |
AGGREGATOR | required | none | Which aggregation provider, if any. The value none means file import only, which is a legitimate final state. |
AGGREGATOR_TOKENsecret | optional | replace-with-provider-token | Provider credential, stored outside the database and never committed. |
STALE_CONNECTION_HOURS | optional | 48 | How long without a refresh before an aggregation connection is reported as stale. |
The build, in order
Importing without duplicating
You can import the same file twice and end up with one copy of each transaction.
Your own, not a sample. Real exports have inconsistent date formats and stray encoding.
Files
src/import/institution.pyEverywhere. Formatting happens at display and nowhere else.
Files
src/money.pyDate, amount, normalised description and account. Record the file hash so a re-import is detected.
Files
src/dedupe.py
done when · tick each as it passeswatch out- Storing money as a floating point number introduces errors that accumulate across thousands of rows and make a report disagree with a statement by a few pence, which destroys trust in the whole thing. Use integer minor units everywhere and format only for display.
- Sign conventions differ between institutions and between account types: some exports show a credit card payment as positive and some as negative. Normalise on import per institution and record the convention, or every credit card total will be inverted.
Reconciling to a statement
Your computed balance matches the bank's, or you can see exactly where it does not.
From statements, per account per date. These are the ground truth you reconcile against.
Files
src/balances.pyCompute running balance and find the first date where it diverges. That narrows the search.
Files
src/reconcile.pyShow the discrepancy. Write a test that deletes a transaction and asserts the gap appears.
Files
tests/test_reconcile.py
done when · tick each as it passeswatch out- A tool that silently inserts a balancing adjustment to make the numbers agree hides the missing transactions that caused the gap, and you will trust a total that is wrong. Show the discrepancy, never correct it automatically.
Transfers, which are neither income nor spending
Money moved between your own accounts stops appearing as both.
Same amount, opposite sign, two of your accounts, within a few days. Propose rather than assume.
Files
src/transfers.pyA wrong automatic pairing is worse than an unpaired transfer, because it hides real spending.
Files
src/transfers.pyConfirmed transfers leave income and expense entirely. Verify totals change by the expected amount.
Files
src/reports.py
done when · tick each as it passeswatch out- Unpaired transfers inflate both income and spending by the same amount, which makes every category total and every budget wrong while the net figure still looks fine. This is the single most common reason a personal finance tool's numbers are quietly untrustworthy.
Categories you can actually fix
Spending is categorised by rules you wrote, and correcting one teaches the rule.
First match wins, with the matching rule recorded on the transaction so a wrong category is traceable.
Files
src/rules.pyOffer, do not assume. A one-off correction and a standing rule are different intentions.
Files
src/correct.pyWith the affected count shown before it runs.
Files
src/recategorise.py
done when · tick each as it passeswatch out- Silently recategorising history whenever a rule changes rewrites last year's totals, so any comparison over time becomes meaningless. Apply new rules going forward and make retroactive recategorisation deliberate, with the affected count shown first.
Budgets and net worth
You can see spending against a plan and what you own against what you owe.
Category spending with transfers excluded and uncategorised shown separately. Verify against a hand calculation.
Files
src/budgets.pyA property or a loan is not in any export and is most of net worth for many people.
Files
src/networth.pyNo interpolation. A missing month should be visible, since it means the data is incomplete.
Files
src/charts.py
done when · tick each as it passeswatch out- Interpolating a net worth line across a period with no data draws a smooth curve through a gap, which looks like information and is invention. Leave gaps visible, because a missing month is something you want to notice.
Automatic aggregation, if you want itproduct builder
Transactions arrive without you exporting files, and you know what that costs and exposes.
The same deduplication and normalisation apply. Aggregation is a source, not a separate system.
Files
src/import/aggregator.pyImport an overlapping period from both a file and the provider. Duplicates here are the failure mode.
Expected refresh window per account. Silence is the dangerous state.
Files
src/health.py
done when · tick each as it passeswatch out- An aggregation connection breaks regularly, usually when the bank changes its authentication, and a tool that fails silently leaves you with a ledger that stopped updating three weeks ago while still showing totals. Alert on a connection that has not refreshed within its expected window.
That is the whole plan for Monarch Money. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Reliable multi-institution bank and investment sync.
- Categorisation trained on many users' corrections.
- Household collaboration with separate logins.
- Investment performance with holdings and cost basis.
- bank/investment sync
- transaction categorization
- household collaboration
- mobile apps
- support
- data security posture
- Multi-currency with dated exchange rates, once you hold an account in a second currency
- Scheduled transaction detection, so a subscription increase is noticed
- A yearly summary that reconciles against your tax records
Need the files? The project pack on the verdict page hands your agent the whole brief · more personal finance.