Vibecode Monarch Money
track this build5 phases, 15 steps, beginner friendly0%A manual finance dashboard is buildable, but high-quality bank aggregation, transaction cleaning, investment data, household collaboration, and mobile sync make it real work.
You are building a lean indie version of Monarch Money. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Monarch Money · indie build 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. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with SQLite | the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows | | Import | File parsers per institution, plus one optional aggregation provider | your bank's export always works and costs nothing, which makes it the baseline rather than the fallback | | Money | Integer minor units, never floating point | a rounding error in a ledger is a bug that compounds silently and destroys trust in every total | | Categorisation | Ordered rules you write, with corrections that create rules | someone else's categories are wrong about your spending and you cannot fix theirs | | Reports | Hand-drawn SVG | a handful of charts with fixed requirements | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - 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 - [ ] **Twelve months of real exports from at least two accounts** · free - 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 - [ ] **Statement balances for those accounts** · free - 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 - [ ] **A decision about automatic aggregation** · nothing 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 - [ ] **An encrypted backup destination** · free 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Monarch Money, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Monarch Money The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal finance tracker that categorises spending and shows net worth from data you actually control to replace Monarch Money. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Decide how transactions arrive before you build anything, because it determines cost, region and effort. Manual import from files your bank already exports always works, costs nothing and is the honest baseline. Automatic aggregation is a paid service in every case worth using, and the options differ sharply by region. As checked in September 2026: SimpleFIN Bridge is around 15 US dollars a year and is aimed at read-only personal finance with daily refresh. Teller offers a developer tier with a limited number of free live connections for United States banks. In Europe, Enable Banking offers free restricted production access limited to accounts you link yourself, while GoCardless was reported in 2025 to have stopped onboarding new bank data customers. Plaid's production access is sales-led. Verify current terms before committing, because this market moves. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with SQLite · the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows - Import: File parsers per institution, plus one optional aggregation provider · your bank's export always works and costs nothing, which makes it the baseline rather than the fallback - Money: Integer minor units, never floating point · a rounding error in a ledger is a bug that compounds silently and destroys trust in every total - Categorisation: Ordered rules you write, with corrections that create rules · someone else's categories are wrong about your spending and you cannot fix theirs - Reports: Hand-drawn SVG · a handful of charts with fixed requirements ### Data model (create this before Phase 1) - 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. ### Phase 1 · Importing without duplicating Build: Parse an export from one real institution into transactions with amounts in integer minor units. Deduplicate on a stable key. Record the import with the file hash so a re-import is recognised. Handle the awkward parts of real exports: date formats, sign conventions and encoding. Trap: 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. Trap: 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. Done when: A real export imports with correct amounts and dates. Importing the same file twice adds nothing and reports the duplicate count. A credit card export's signs are normalised so spending is negative and payments positive. No amount is stored as a floating point number, verified by inspecting the schema. Do not build yet: No categorisation, no reconciliation, no reports. ### Phase 2 · Reconciling to a statement Build: Record asserted balances from statements. Compute the balance from transactions and compare. Where they disagree, show the gap and the period it appeared in, rather than adjusting anything to make it match. Trap: 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. Done when: Computed and asserted balances match for a fully imported account. Deliberately deleting a transaction produces a visible discrepancy naming the amount and the period. No automatic adjustment is ever created. An account with a gap in its import history is marked as incompletely reconciled. Do not build yet: No categorisation or budgets yet. ### Phase 3 · Transfers, which are neither income nor spending Build: Detect pairs of transactions that are the same amount in opposite directions across two of your accounts within a few days. Propose them as transfers, require confirmation, and exclude confirmed transfers from income and expense totals. Trap: 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. Done when: A transfer between two imported accounts is proposed as a pair and, once confirmed, appears in neither income nor expense totals. A payment to an external account is not proposed as a transfer. Total income and total expense both fall by the transfer amount after confirmation. Unconfirmed candidates are visible rather than silently ignored. Do not build yet: No categorisation rules yet. ### Phase 4 · Categories you can actually fix Build: Ordered rules matching on description with an explicit uncategorised fallback. Correcting a transaction's category offers to create or amend a rule. Changing rules does not silently rewrite history; recategorising is an explicit action reporting how many transactions it will change. Trap: 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. Done when: A rule categorises matching transactions and the matching rule is shown on each. Correcting a category offers to create a rule and the offer produces one that matches the same merchant next month. Changing a rule leaves history alone until recategorisation is run. Uncategorised spending appears as its own line in every report. Do not build yet: No budgets or reports yet. ### Phase 5 · Budgets and net worth Build: Budgets per category per period, compared against actual spending with transfers excluded. Net worth from account balances including manually entered assets and liabilities. Show gaps in data rather than interpolating across them. Trap: 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. Done when: A budget compares against actual spending with transfers excluded, matching a hand calculation for one month. Net worth includes manually entered assets and updates when a balance is asserted. A month with no imported data appears as a gap rather than a straight line. Changing a budget does not alter historical actuals. Do not build yet: Aggregation and backups come last. ### Phase 6 · Automatic aggregation, if you want it Build: Add one aggregation provider chosen for your region, behind the same import interface the file parsers use. Store credentials outside the database. Handle the connection breaking, which it will. Then record the annual cost and what the provider can see. Trap: 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. Done when: Transactions arrive automatically and deduplicate correctly against previously imported files, verified by importing an overlapping period both ways. A broken connection produces an alert rather than silence. Credentials are outside the database. The annual cost and the provider's data access are written down. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable bank and investment sync across many institutions, which is the paid service and breaks constantly even when someone else is maintaining it. - Transaction categorisation trained on many users' corrections, which is why the commercial product is right more often on a merchant you have never seen. - Household collaboration with separate logins and shared visibility rules. - Investment performance analysis with holdings, cost basis and returns, which is a substantially larger project than a spending tracker. ### README must contain - That money is stored in integer minor units and never as floating point - That reconciliation shows discrepancies and never creates a balancing adjustment - That transfers must be paired or every category total is inflated - That changing a rule does not rewrite history until recategorisation is run explicitly - Which aggregation provider is used, what it costs annually, and what it can see ===== AGENTS.md ===== # Agent instructions · Monarch Money indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with SQLite, File parsers per institution, plus one optional aggregation provider, Integer minor units, never floating point, Ordered rules you write, with corrections that create rules, Hand-drawn SVG. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Monarch Money 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing without duplicating You can import the same file twice and end up with one copy of each transaction. ### Steps 1. Parse one real export end to end Your own, not a sample. Real exports have inconsistent date formats and stray encoding. Files: `src/import/institution.py` 2. Store integer minor units Everywhere. Formatting happens at display and nowhere else. Files: `src/money.py` 3. Deduplicate on a stable key Date, amount, normalised description and account. Record the file hash so a re-import is detected. Files: `src/dedupe.py` ### Done when - [ ] A real export imports with correct amounts and dates - [ ] Re-importing the same file adds nothing and reports duplicates - [ ] Credit card signs are normalised correctly - [ ] No monetary value is stored as floating point ### Watch 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. ## Phase 2 · Reconciling to a statement Your computed balance matches the bank's, or you can see exactly where it does not. ### Steps 1. Record asserted balances From statements, per account per date. These are the ground truth you reconcile against. Files: `src/balances.py` 2. Compare and localise the gap Compute running balance and find the first date where it diverges. That narrows the search. Files: `src/reconcile.py` 3. Never auto-adjust Show the discrepancy. Write a test that deletes a transaction and asserts the gap appears. Files: `tests/test_reconcile.py` ### Done when - [ ] Balances match for a fully imported account - [ ] A deleted transaction produces a visible discrepancy - [ ] No balancing adjustment is ever created automatically - [ ] An account with import gaps is marked incompletely reconciled ### Watch 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. ## Phase 3 · Transfers, which are neither income nor spending Money moved between your own accounts stops appearing as both. ### Steps 1. Detect candidate pairs Same amount, opposite sign, two of your accounts, within a few days. Propose rather than assume. Files: `src/transfers.py` 2. Require confirmation A wrong automatic pairing is worse than an unpaired transfer, because it hides real spending. Files: `src/transfers.py` 3. Exclude from totals Confirmed transfers leave income and expense entirely. Verify totals change by the expected amount. Files: `src/reports.py` ### Done when - [ ] A genuine transfer is proposed and pairs on confirmation - [ ] An external payment is not proposed as a transfer - [ ] Confirming reduces both income and expense totals correctly - [ ] Unconfirmed candidates remain visible ### Watch 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. ## Phase 4 · Categories you can actually fix Spending is categorised by rules you wrote, and correcting one teaches the rule. ### Steps 1. Evaluate rules in order First match wins, with the matching rule recorded on the transaction so a wrong category is traceable. Files: `src/rules.py` 2. Turn corrections into rules Offer, do not assume. A one-off correction and a standing rule are different intentions. Files: `src/correct.py` 3. Make recategorisation explicit With the affected count shown before it runs. Files: `src/recategorise.py` ### Done when - [ ] Each transaction shows the rule that categorised it - [ ] A correction can create a rule that matches next month - [ ] Changing a rule leaves history alone until recategorisation - [ ] Uncategorised spending is visible in reports ### Watch 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. ## Phase 5 · Budgets and net worth You can see spending against a plan and what you own against what you owe. ### Steps 1. Compute budget actuals correctly Category spending with transfers excluded and uncategorised shown separately. Verify against a hand calculation. Files: `src/budgets.py` 2. Add manual assets and liabilities A property or a loan is not in any export and is most of net worth for many people. Files: `src/networth.py` 3. Leave gaps as gaps No interpolation. A missing month should be visible, since it means the data is incomplete. Files: `src/charts.py` ### Done when - [ ] A budget's actuals match a hand calculation - [ ] Net worth includes manual assets and updates on new balances - [ ] A month with no data appears as a gap - [ ] Changing a budget does not alter historical actuals ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The ledger. A complete record of your finances, so treat the file and its backups accordingly. DB_PATH=./data/ledger.db # Required. Reporting currency. Multi-currency accounts convert into it for totals. BASE_CURRENCY=GBP # Required. How far apart two sides of a transfer may be before they stop being proposed as a pair. TRANSFER_WINDOW_DAYS=4 # Required. Which aggregation provider, if any. The value none means file import only, which is a legitimate final state. AGGREGATOR=none # Optional · secret. Provider credential, stored outside the database and never committed. AGGREGATOR_TOKEN=replace-with-provider-token # Optional. How long without a refresh before an aggregation connection is reported as stale. STALE_CONNECTION_HOURS=48
You are building a lean indie version of Monarch Money. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Monarch Money · indie build 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. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with SQLite | the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows | | Import | File parsers per institution, plus one optional aggregation provider | your bank's export always works and costs nothing, which makes it the baseline rather than the fallback | | Money | Integer minor units, never floating point | a rounding error in a ledger is a bug that compounds silently and destroys trust in every total | | Categorisation | Ordered rules you write, with corrections that create rules | someone else's categories are wrong about your spending and you cannot fix theirs | | Reports | Hand-drawn SVG | a handful of charts with fixed requirements | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - 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 - [ ] **Twelve months of real exports from at least two accounts** · free - 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 - [ ] **Statement balances for those accounts** · free - 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 - [ ] **A decision about automatic aggregation** · nothing 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 - [ ] **An encrypted backup destination** · free 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Monarch Money, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Monarch Money The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal finance tracker that categorises spending and shows net worth from data you actually control to replace Monarch Money. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Decide how transactions arrive before you build anything, because it determines cost, region and effort. Manual import from files your bank already exports always works, costs nothing and is the honest baseline. Automatic aggregation is a paid service in every case worth using, and the options differ sharply by region. As checked in September 2026: SimpleFIN Bridge is around 15 US dollars a year and is aimed at read-only personal finance with daily refresh. Teller offers a developer tier with a limited number of free live connections for United States banks. In Europe, Enable Banking offers free restricted production access limited to accounts you link yourself, while GoCardless was reported in 2025 to have stopped onboarding new bank data customers. Plaid's production access is sales-led. Verify current terms before committing, because this market moves. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with SQLite · the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows - Import: File parsers per institution, plus one optional aggregation provider · your bank's export always works and costs nothing, which makes it the baseline rather than the fallback - Money: Integer minor units, never floating point · a rounding error in a ledger is a bug that compounds silently and destroys trust in every total - Categorisation: Ordered rules you write, with corrections that create rules · someone else's categories are wrong about your spending and you cannot fix theirs - Reports: Hand-drawn SVG · a handful of charts with fixed requirements ### Data model (create this before Phase 1) - 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. ### Phase 1 · Importing without duplicating Build: Parse an export from one real institution into transactions with amounts in integer minor units. Deduplicate on a stable key. Record the import with the file hash so a re-import is recognised. Handle the awkward parts of real exports: date formats, sign conventions and encoding. Trap: 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. Trap: 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. Done when: A real export imports with correct amounts and dates. Importing the same file twice adds nothing and reports the duplicate count. A credit card export's signs are normalised so spending is negative and payments positive. No amount is stored as a floating point number, verified by inspecting the schema. Do not build yet: No categorisation, no reconciliation, no reports. ### Phase 2 · Reconciling to a statement Build: Record asserted balances from statements. Compute the balance from transactions and compare. Where they disagree, show the gap and the period it appeared in, rather than adjusting anything to make it match. Trap: 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. Done when: Computed and asserted balances match for a fully imported account. Deliberately deleting a transaction produces a visible discrepancy naming the amount and the period. No automatic adjustment is ever created. An account with a gap in its import history is marked as incompletely reconciled. Do not build yet: No categorisation or budgets yet. ### Phase 3 · Transfers, which are neither income nor spending Build: Detect pairs of transactions that are the same amount in opposite directions across two of your accounts within a few days. Propose them as transfers, require confirmation, and exclude confirmed transfers from income and expense totals. Trap: 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. Done when: A transfer between two imported accounts is proposed as a pair and, once confirmed, appears in neither income nor expense totals. A payment to an external account is not proposed as a transfer. Total income and total expense both fall by the transfer amount after confirmation. Unconfirmed candidates are visible rather than silently ignored. Do not build yet: No categorisation rules yet. ### Phase 4 · Categories you can actually fix Build: Ordered rules matching on description with an explicit uncategorised fallback. Correcting a transaction's category offers to create or amend a rule. Changing rules does not silently rewrite history; recategorising is an explicit action reporting how many transactions it will change. Trap: 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. Done when: A rule categorises matching transactions and the matching rule is shown on each. Correcting a category offers to create a rule and the offer produces one that matches the same merchant next month. Changing a rule leaves history alone until recategorisation is run. Uncategorised spending appears as its own line in every report. Do not build yet: No budgets or reports yet. ### Phase 5 · Budgets and net worth Build: Budgets per category per period, compared against actual spending with transfers excluded. Net worth from account balances including manually entered assets and liabilities. Show gaps in data rather than interpolating across them. Trap: 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. Done when: A budget compares against actual spending with transfers excluded, matching a hand calculation for one month. Net worth includes manually entered assets and updates when a balance is asserted. A month with no imported data appears as a gap rather than a straight line. Changing a budget does not alter historical actuals. Do not build yet: Aggregation and backups come last. ### Phase 6 · Automatic aggregation, if you want it Build: Add one aggregation provider chosen for your region, behind the same import interface the file parsers use. Store credentials outside the database. Handle the connection breaking, which it will. Then record the annual cost and what the provider can see. Trap: 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. Done when: Transactions arrive automatically and deduplicate correctly against previously imported files, verified by importing an overlapping period both ways. A broken connection produces an alert rather than silence. Credentials are outside the database. The annual cost and the provider's data access are written down. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable bank and investment sync across many institutions, which is the paid service and breaks constantly even when someone else is maintaining it. - Transaction categorisation trained on many users' corrections, which is why the commercial product is right more often on a merchant you have never seen. - Household collaboration with separate logins and shared visibility rules. - Investment performance analysis with holdings, cost basis and returns, which is a substantially larger project than a spending tracker. ### README must contain - That money is stored in integer minor units and never as floating point - That reconciliation shows discrepancies and never creates a balancing adjustment - That transfers must be paired or every category total is inflated - That changing a rule does not rewrite history until recategorisation is run explicitly - Which aggregation provider is used, what it costs annually, and what it can see ===== AGENTS.md ===== # Agent instructions · Monarch Money indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with SQLite, File parsers per institution, plus one optional aggregation provider, Integer minor units, never floating point, Ordered rules you write, with corrections that create rules, Hand-drawn SVG. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Monarch Money 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing without duplicating You can import the same file twice and end up with one copy of each transaction. ### Steps 1. Parse one real export end to end Your own, not a sample. Real exports have inconsistent date formats and stray encoding. Files: `src/import/institution.py` 2. Store integer minor units Everywhere. Formatting happens at display and nowhere else. Files: `src/money.py` 3. Deduplicate on a stable key Date, amount, normalised description and account. Record the file hash so a re-import is detected. Files: `src/dedupe.py` ### Done when - [ ] A real export imports with correct amounts and dates - [ ] Re-importing the same file adds nothing and reports duplicates - [ ] Credit card signs are normalised correctly - [ ] No monetary value is stored as floating point ### Watch 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. ## Phase 2 · Reconciling to a statement Your computed balance matches the bank's, or you can see exactly where it does not. ### Steps 1. Record asserted balances From statements, per account per date. These are the ground truth you reconcile against. Files: `src/balances.py` 2. Compare and localise the gap Compute running balance and find the first date where it diverges. That narrows the search. Files: `src/reconcile.py` 3. Never auto-adjust Show the discrepancy. Write a test that deletes a transaction and asserts the gap appears. Files: `tests/test_reconcile.py` ### Done when - [ ] Balances match for a fully imported account - [ ] A deleted transaction produces a visible discrepancy - [ ] No balancing adjustment is ever created automatically - [ ] An account with import gaps is marked incompletely reconciled ### Watch 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. ## Phase 3 · Transfers, which are neither income nor spending Money moved between your own accounts stops appearing as both. ### Steps 1. Detect candidate pairs Same amount, opposite sign, two of your accounts, within a few days. Propose rather than assume. Files: `src/transfers.py` 2. Require confirmation A wrong automatic pairing is worse than an unpaired transfer, because it hides real spending. Files: `src/transfers.py` 3. Exclude from totals Confirmed transfers leave income and expense entirely. Verify totals change by the expected amount. Files: `src/reports.py` ### Done when - [ ] A genuine transfer is proposed and pairs on confirmation - [ ] An external payment is not proposed as a transfer - [ ] Confirming reduces both income and expense totals correctly - [ ] Unconfirmed candidates remain visible ### Watch 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. ## Phase 4 · Categories you can actually fix Spending is categorised by rules you wrote, and correcting one teaches the rule. ### Steps 1. Evaluate rules in order First match wins, with the matching rule recorded on the transaction so a wrong category is traceable. Files: `src/rules.py` 2. Turn corrections into rules Offer, do not assume. A one-off correction and a standing rule are different intentions. Files: `src/correct.py` 3. Make recategorisation explicit With the affected count shown before it runs. Files: `src/recategorise.py` ### Done when - [ ] Each transaction shows the rule that categorised it - [ ] A correction can create a rule that matches next month - [ ] Changing a rule leaves history alone until recategorisation - [ ] Uncategorised spending is visible in reports ### Watch 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. ## Phase 5 · Budgets and net worth You can see spending against a plan and what you own against what you owe. ### Steps 1. Compute budget actuals correctly Category spending with transfers excluded and uncategorised shown separately. Verify against a hand calculation. Files: `src/budgets.py` 2. Add manual assets and liabilities A property or a loan is not in any export and is most of net worth for many people. Files: `src/networth.py` 3. Leave gaps as gaps No interpolation. A missing month should be visible, since it means the data is incomplete. Files: `src/charts.py` ### Done when - [ ] A budget's actuals match a hand calculation - [ ] Net worth includes manual assets and updates on new balances - [ ] A month with no data appears as a gap - [ ] Changing a budget does not alter historical actuals ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The ledger. A complete record of your finances, so treat the file and its backups accordingly. DB_PATH=./data/ledger.db # Required. Reporting currency. Multi-currency accounts convert into it for totals. BASE_CURRENCY=GBP # Required. How far apart two sides of a transfer may be before they stop being proposed as a pair. TRANSFER_WINDOW_DAYS=4 # Required. Which aggregation provider, if any. The value none means file import only, which is a legitimate final state. AGGREGATOR=none # Optional · secret. Provider credential, stored outside the database and never committed. AGGREGATOR_TOKEN=replace-with-provider-token # Optional. How long without a refresh before an aggregation connection is reported as stale. STALE_CONNECTION_HOURS=48
You are building a production product version of Monarch Money. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Monarch Money · product brief ## Problem A manual finance dashboard is buildable, but high-quality bank aggregation, transaction cleaning, investment data, household collaboration, and mobile sync make it real work. ## Product outcome A ledger whose totals reconcile to your statements, where transfers do not inflate spending, where categories are yours and correctable, and where a broken data connection tells you rather than quietly freezing your numbers. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - bank aggregation API or CSV import - secure database - categorization rules - charts - mobile/web app - backups ## Explicit non-goals for v1 - 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 ## Success criteria - No monetary value is stored as floating point - A deleted transaction is confirmed to produce a visible discrepancy - Transfer confirmation is confirmed to reduce both income and expense - An overlapping file and aggregation import is confirmed not to duplicate - An encrypted backup has been restored and reconciled ===== BRIEF.md ===== # Build brief · Monarch Money The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal finance tracker that categorises spending and shows net worth from data you actually control to replace Monarch Money. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Decide how transactions arrive before you build anything, because it determines cost, region and effort. Manual import from files your bank already exports always works, costs nothing and is the honest baseline. Automatic aggregation is a paid service in every case worth using, and the options differ sharply by region. As checked in September 2026: SimpleFIN Bridge is around 15 US dollars a year and is aimed at read-only personal finance with daily refresh. Teller offers a developer tier with a limited number of free live connections for United States banks. In Europe, Enable Banking offers free restricted production access limited to accounts you link yourself, while GoCardless was reported in 2025 to have stopped onboarding new bank data customers. Plaid's production access is sales-led. Verify current terms before committing, because this market moves. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with SQLite · the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows - Import: File parsers per institution, plus one optional aggregation provider · your bank's export always works and costs nothing, which makes it the baseline rather than the fallback - Money: Integer minor units, never floating point · a rounding error in a ledger is a bug that compounds silently and destroys trust in every total - Categorisation: Ordered rules you write, with corrections that create rules · someone else's categories are wrong about your spending and you cannot fix theirs - Reports: Hand-drawn SVG · a handful of charts with fixed requirements ### Data model (create this before Phase 1) - 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. ### Phase 1 · Importing without duplicating Build: Parse an export from one real institution into transactions with amounts in integer minor units. Deduplicate on a stable key. Record the import with the file hash so a re-import is recognised. Handle the awkward parts of real exports: date formats, sign conventions and encoding. Trap: 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. Trap: 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. Done when: A real export imports with correct amounts and dates. Importing the same file twice adds nothing and reports the duplicate count. A credit card export's signs are normalised so spending is negative and payments positive. No amount is stored as a floating point number, verified by inspecting the schema. Do not build yet: No categorisation, no reconciliation, no reports. ### Phase 2 · Reconciling to a statement Build: Record asserted balances from statements. Compute the balance from transactions and compare. Where they disagree, show the gap and the period it appeared in, rather than adjusting anything to make it match. Trap: 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. Done when: Computed and asserted balances match for a fully imported account. Deliberately deleting a transaction produces a visible discrepancy naming the amount and the period. No automatic adjustment is ever created. An account with a gap in its import history is marked as incompletely reconciled. Do not build yet: No categorisation or budgets yet. ### Phase 3 · Transfers, which are neither income nor spending Build: Detect pairs of transactions that are the same amount in opposite directions across two of your accounts within a few days. Propose them as transfers, require confirmation, and exclude confirmed transfers from income and expense totals. Trap: 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. Done when: A transfer between two imported accounts is proposed as a pair and, once confirmed, appears in neither income nor expense totals. A payment to an external account is not proposed as a transfer. Total income and total expense both fall by the transfer amount after confirmation. Unconfirmed candidates are visible rather than silently ignored. Do not build yet: No categorisation rules yet. ### Phase 4 · Categories you can actually fix Build: Ordered rules matching on description with an explicit uncategorised fallback. Correcting a transaction's category offers to create or amend a rule. Changing rules does not silently rewrite history; recategorising is an explicit action reporting how many transactions it will change. Trap: 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. Done when: A rule categorises matching transactions and the matching rule is shown on each. Correcting a category offers to create a rule and the offer produces one that matches the same merchant next month. Changing a rule leaves history alone until recategorisation is run. Uncategorised spending appears as its own line in every report. Do not build yet: No budgets or reports yet. ### Phase 5 · Budgets and net worth Build: Budgets per category per period, compared against actual spending with transfers excluded. Net worth from account balances including manually entered assets and liabilities. Show gaps in data rather than interpolating across them. Trap: 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. Done when: A budget compares against actual spending with transfers excluded, matching a hand calculation for one month. Net worth includes manually entered assets and updates when a balance is asserted. A month with no imported data appears as a gap rather than a straight line. Changing a budget does not alter historical actuals. Do not build yet: Aggregation and backups come last. ### Phase 6 · Automatic aggregation, if you want it Build: Add one aggregation provider chosen for your region, behind the same import interface the file parsers use. Store credentials outside the database. Handle the connection breaking, which it will. Then record the annual cost and what the provider can see. Trap: 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. Done when: Transactions arrive automatically and deduplicate correctly against previously imported files, verified by importing an overlapping period both ways. A broken connection produces an alert rather than silence. Credentials are outside the database. The annual cost and the provider's data access are written down. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable bank and investment sync across many institutions, which is the paid service and breaks constantly even when someone else is maintaining it. - Transaction categorisation trained on many users' corrections, which is why the commercial product is right more often on a merchant you have never seen. - Household collaboration with separate logins and shared visibility rules. - Investment performance analysis with holdings, cost basis and returns, which is a substantially larger project than a spending tracker. ### README must contain - That money is stored in integer minor units and never as floating point - That reconciliation shows discrepancies and never creates a balancing adjustment - That transfers must be paired or every category total is inflated - That changing a rule does not rewrite history until recategorisation is run explicitly - Which aggregation provider is used, what it costs annually, and what it can see ===== ARCHITECTURE.md ===== # Architecture · Monarch Money ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with SQLite | the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows | | Import | File parsers per institution, plus one optional aggregation provider | your bank's export always works and costs nothing, which makes it the baseline rather than the fallback | | Money | Integer minor units, never floating point | a rounding error in a ledger is a bug that compounds silently and destroys trust in every total | | Categorisation | Ordered rules you write, with corrections that create rules | someone else's categories are wrong about your spending and you cannot fix theirs | | Reports | Hand-drawn SVG | a handful of charts with fixed requirements | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Parsing sources into normalised transactions. | File parsers and aggregation share one interface, which is why adding a provider does not change deduplication or normalisation. | | Money | Integer minor units and formatting. | Not swappable for floating point under any circumstances. This is the invariant everything else rests on. | | Reconcile | Comparing computed and asserted balances. | Any implementation, provided it never adjusts to make numbers agree. | | Transfers | Pairing internal movements. | Detection can improve; requiring confirmation should not, because a wrong pairing hides real spending. | | Rules | Categorisation and its explanation. | Ordered rules are debuggable in a way a model is not. If you add a model, keep it proposing rather than deciding. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · The ledger. A complete record of your finances, so treat the file and its backups accordingly. - `BASE_CURRENCY` · required · Reporting currency. Multi-currency accounts convert into it for totals. - `TRANSFER_WINDOW_DAYS` · required · How far apart two sides of a transfer may be before they stop being proposed as a pair. - `AGGREGATOR` · required · Which aggregation provider, if any. The value none means file import only, which is a legitimate final state. - `AGGREGATOR_TOKEN` · optional, secret · Provider credential, stored outside the database and never committed. - `STALE_CONNECTION_HOURS` · optional · How long without a refresh before an aggregation connection is reported as stale. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Monarch Money product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with SQLite, File parsers per institution, plus one optional aggregation provider, Integer minor units, never floating point, Ordered rules you write, with corrections that create rules, Hand-drawn SVG. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · Monarch Money Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Importing without duplicating You can import the same file twice and end up with one copy of each transaction. ### Steps 1. Parse one real export end to end Your own, not a sample. Real exports have inconsistent date formats and stray encoding. Files: `src/import/institution.py` 2. Store integer minor units Everywhere. Formatting happens at display and nowhere else. Files: `src/money.py` 3. Deduplicate on a stable key Date, amount, normalised description and account. Record the file hash so a re-import is detected. Files: `src/dedupe.py` ### Done when - [ ] A real export imports with correct amounts and dates - [ ] Re-importing the same file adds nothing and reports duplicates - [ ] Credit card signs are normalised correctly - [ ] No monetary value is stored as floating point ### Watch 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. ## M2 · Reconciling to a statement Your computed balance matches the bank's, or you can see exactly where it does not. ### Steps 1. Record asserted balances From statements, per account per date. These are the ground truth you reconcile against. Files: `src/balances.py` 2. Compare and localise the gap Compute running balance and find the first date where it diverges. That narrows the search. Files: `src/reconcile.py` 3. Never auto-adjust Show the discrepancy. Write a test that deletes a transaction and asserts the gap appears. Files: `tests/test_reconcile.py` ### Done when - [ ] Balances match for a fully imported account - [ ] A deleted transaction produces a visible discrepancy - [ ] No balancing adjustment is ever created automatically - [ ] An account with import gaps is marked incompletely reconciled ### Watch 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. ## M3 · Transfers, which are neither income nor spending Money moved between your own accounts stops appearing as both. ### Steps 1. Detect candidate pairs Same amount, opposite sign, two of your accounts, within a few days. Propose rather than assume. Files: `src/transfers.py` 2. Require confirmation A wrong automatic pairing is worse than an unpaired transfer, because it hides real spending. Files: `src/transfers.py` 3. Exclude from totals Confirmed transfers leave income and expense entirely. Verify totals change by the expected amount. Files: `src/reports.py` ### Done when - [ ] A genuine transfer is proposed and pairs on confirmation - [ ] An external payment is not proposed as a transfer - [ ] Confirming reduces both income and expense totals correctly - [ ] Unconfirmed candidates remain visible ### Watch 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. ## M4 · Categories you can actually fix Spending is categorised by rules you wrote, and correcting one teaches the rule. ### Steps 1. Evaluate rules in order First match wins, with the matching rule recorded on the transaction so a wrong category is traceable. Files: `src/rules.py` 2. Turn corrections into rules Offer, do not assume. A one-off correction and a standing rule are different intentions. Files: `src/correct.py` 3. Make recategorisation explicit With the affected count shown before it runs. Files: `src/recategorise.py` ### Done when - [ ] Each transaction shows the rule that categorised it - [ ] A correction can create a rule that matches next month - [ ] Changing a rule leaves history alone until recategorisation - [ ] Uncategorised spending is visible in reports ### Watch 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. ## M5 · Budgets and net worth You can see spending against a plan and what you own against what you owe. ### Steps 1. Compute budget actuals correctly Category spending with transfers excluded and uncategorised shown separately. Verify against a hand calculation. Files: `src/budgets.py` 2. Add manual assets and liabilities A property or a loan is not in any export and is most of net worth for many people. Files: `src/networth.py` 3. Leave gaps as gaps No interpolation. A missing month should be visible, since it means the data is incomplete. Files: `src/charts.py` ### Done when - [ ] A budget's actuals match a hand calculation - [ ] Net worth includes manual assets and updates on new balances - [ ] A month with no data appears as a gap - [ ] Changing a budget does not alter historical actuals ### Watch 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. ## M6 · Automatic aggregation, if you want it (production only) Transactions arrive without you exporting files, and you know what that costs and exposes. ### Steps 1. Add one provider behind the import interface The same deduplication and normalisation apply. Aggregation is a source, not a separate system. Files: `src/import/aggregator.py` 2. Test the overlap with file imports Import an overlapping period from both a file and the provider. Duplicates here are the failure mode. 3. Alert on a stale connection Expected refresh window per account. Silence is the dangerous state. Files: `src/health.py` ### Done when - [ ] Automatic and file imports of an overlapping period do not duplicate - [ ] A stale connection produces an alert - [ ] Credentials live outside the database - [ ] The annual cost and provider access are documented ### Watch 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. ===== OPERATIONS.md ===== # Operations · Monarch Money ## Backup The ledger is a complete financial record. Back it up encrypted, to a destination you control, and restore it once so you know the encryption key works. ## Restore Restore the database and confirm a recent month reconciles against a statement. That check is what proves the restore rather than the file merely opening. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Aggregation connection freshness and reconciliation discrepancies. A growing discrepancy usually means an import gap rather than an arithmetic bug, and it is worth catching before you rely on a report. ## Incident checklist If totals look wrong, check transfer pairing before anything else, since unpaired transfers are the most common cause. If a connection stopped, the alert should already have fired; if it did not, that is the first bug to fix. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No monetary value is stored as floating point - [ ] A deleted transaction is confirmed to produce a visible discrepancy - [ ] Transfer confirmation is confirmed to reduce both income and expense - [ ] An overlapping file and aggregation import is confirmed not to duplicate - [ ] An encrypted backup has been restored and reconciled ## Launch constraint Do not market omitted Monarch Money capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The ledger. A complete record of your finances, so treat the file and its backups accordingly. DB_PATH=./data/ledger.db # Required. Reporting currency. Multi-currency accounts convert into it for totals. BASE_CURRENCY=GBP # Required. How far apart two sides of a transfer may be before they stop being proposed as a pair. TRANSFER_WINDOW_DAYS=4 # Required. Which aggregation provider, if any. The value none means file import only, which is a legitimate final state. AGGREGATOR=none # Optional · secret. Provider credential, stored outside the database and never committed. AGGREGATOR_TOKEN=replace-with-provider-token # Optional. How long without a refresh before an aggregation connection is reported as stale. STALE_CONNECTION_HOURS=48
# Monarch Money · indie build 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. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with SQLite | the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows | | Import | File parsers per institution, plus one optional aggregation provider | your bank's export always works and costs nothing, which makes it the baseline rather than the fallback | | Money | Integer minor units, never floating point | a rounding error in a ledger is a bug that compounds silently and destroys trust in every total | | Categorisation | Ordered rules you write, with corrections that create rules | someone else's categories are wrong about your spending and you cannot fix theirs | | Reports | Hand-drawn SVG | a handful of charts with fixed requirements | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - 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 - [ ] **Twelve months of real exports from at least two accounts** · free - 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 - [ ] **Statement balances for those accounts** · free - 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 - [ ] **A decision about automatic aggregation** · nothing 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 - [ ] **An encrypted backup destination** · free 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Monarch Money, and the README should say so rather than pretend.
# Build brief · Monarch Money The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal finance tracker that categorises spending and shows net worth from data you actually control to replace Monarch Money. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Decide how transactions arrive before you build anything, because it determines cost, region and effort. Manual import from files your bank already exports always works, costs nothing and is the honest baseline. Automatic aggregation is a paid service in every case worth using, and the options differ sharply by region. As checked in September 2026: SimpleFIN Bridge is around 15 US dollars a year and is aimed at read-only personal finance with daily refresh. Teller offers a developer tier with a limited number of free live connections for United States banks. In Europe, Enable Banking offers free restricted production access limited to accounts you link yourself, while GoCardless was reported in 2025 to have stopped onboarding new bank data customers. Plaid's production access is sales-led. Verify current terms before committing, because this market moves. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with SQLite · the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows - Import: File parsers per institution, plus one optional aggregation provider · your bank's export always works and costs nothing, which makes it the baseline rather than the fallback - Money: Integer minor units, never floating point · a rounding error in a ledger is a bug that compounds silently and destroys trust in every total - Categorisation: Ordered rules you write, with corrections that create rules · someone else's categories are wrong about your spending and you cannot fix theirs - Reports: Hand-drawn SVG · a handful of charts with fixed requirements ### Data model (create this before Phase 1) - 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. ### Phase 1 · Importing without duplicating Build: Parse an export from one real institution into transactions with amounts in integer minor units. Deduplicate on a stable key. Record the import with the file hash so a re-import is recognised. Handle the awkward parts of real exports: date formats, sign conventions and encoding. Trap: 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. Trap: 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. Done when: A real export imports with correct amounts and dates. Importing the same file twice adds nothing and reports the duplicate count. A credit card export's signs are normalised so spending is negative and payments positive. No amount is stored as a floating point number, verified by inspecting the schema. Do not build yet: No categorisation, no reconciliation, no reports. ### Phase 2 · Reconciling to a statement Build: Record asserted balances from statements. Compute the balance from transactions and compare. Where they disagree, show the gap and the period it appeared in, rather than adjusting anything to make it match. Trap: 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. Done when: Computed and asserted balances match for a fully imported account. Deliberately deleting a transaction produces a visible discrepancy naming the amount and the period. No automatic adjustment is ever created. An account with a gap in its import history is marked as incompletely reconciled. Do not build yet: No categorisation or budgets yet. ### Phase 3 · Transfers, which are neither income nor spending Build: Detect pairs of transactions that are the same amount in opposite directions across two of your accounts within a few days. Propose them as transfers, require confirmation, and exclude confirmed transfers from income and expense totals. Trap: 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. Done when: A transfer between two imported accounts is proposed as a pair and, once confirmed, appears in neither income nor expense totals. A payment to an external account is not proposed as a transfer. Total income and total expense both fall by the transfer amount after confirmation. Unconfirmed candidates are visible rather than silently ignored. Do not build yet: No categorisation rules yet. ### Phase 4 · Categories you can actually fix Build: Ordered rules matching on description with an explicit uncategorised fallback. Correcting a transaction's category offers to create or amend a rule. Changing rules does not silently rewrite history; recategorising is an explicit action reporting how many transactions it will change. Trap: 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. Done when: A rule categorises matching transactions and the matching rule is shown on each. Correcting a category offers to create a rule and the offer produces one that matches the same merchant next month. Changing a rule leaves history alone until recategorisation is run. Uncategorised spending appears as its own line in every report. Do not build yet: No budgets or reports yet. ### Phase 5 · Budgets and net worth Build: Budgets per category per period, compared against actual spending with transfers excluded. Net worth from account balances including manually entered assets and liabilities. Show gaps in data rather than interpolating across them. Trap: 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. Done when: A budget compares against actual spending with transfers excluded, matching a hand calculation for one month. Net worth includes manually entered assets and updates when a balance is asserted. A month with no imported data appears as a gap rather than a straight line. Changing a budget does not alter historical actuals. Do not build yet: Aggregation and backups come last. ### Phase 6 · Automatic aggregation, if you want it Build: Add one aggregation provider chosen for your region, behind the same import interface the file parsers use. Store credentials outside the database. Handle the connection breaking, which it will. Then record the annual cost and what the provider can see. Trap: 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. Done when: Transactions arrive automatically and deduplicate correctly against previously imported files, verified by importing an overlapping period both ways. A broken connection produces an alert rather than silence. Credentials are outside the database. The annual cost and the provider's data access are written down. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable bank and investment sync across many institutions, which is the paid service and breaks constantly even when someone else is maintaining it. - Transaction categorisation trained on many users' corrections, which is why the commercial product is right more often on a merchant you have never seen. - Household collaboration with separate logins and shared visibility rules. - Investment performance analysis with holdings, cost basis and returns, which is a substantially larger project than a spending tracker. ### README must contain - That money is stored in integer minor units and never as floating point - That reconciliation shows discrepancies and never creates a balancing adjustment - That transfers must be paired or every category total is inflated - That changing a rule does not rewrite history until recategorisation is run explicitly - Which aggregation provider is used, what it costs annually, and what it can see
# Agent instructions · Monarch Money indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with SQLite, File parsers per institution, plus one optional aggregation provider, Integer minor units, never floating point, Ordered rules you write, with corrections that create rules, Hand-drawn SVG. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. - 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.
# Build plan · Monarch Money 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing without duplicating You can import the same file twice and end up with one copy of each transaction. ### Steps 1. Parse one real export end to end Your own, not a sample. Real exports have inconsistent date formats and stray encoding. Files: `src/import/institution.py` 2. Store integer minor units Everywhere. Formatting happens at display and nowhere else. Files: `src/money.py` 3. Deduplicate on a stable key Date, amount, normalised description and account. Record the file hash so a re-import is detected. Files: `src/dedupe.py` ### Done when - [ ] A real export imports with correct amounts and dates - [ ] Re-importing the same file adds nothing and reports duplicates - [ ] Credit card signs are normalised correctly - [ ] No monetary value is stored as floating point ### Watch 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. ## Phase 2 · Reconciling to a statement Your computed balance matches the bank's, or you can see exactly where it does not. ### Steps 1. Record asserted balances From statements, per account per date. These are the ground truth you reconcile against. Files: `src/balances.py` 2. Compare and localise the gap Compute running balance and find the first date where it diverges. That narrows the search. Files: `src/reconcile.py` 3. Never auto-adjust Show the discrepancy. Write a test that deletes a transaction and asserts the gap appears. Files: `tests/test_reconcile.py` ### Done when - [ ] Balances match for a fully imported account - [ ] A deleted transaction produces a visible discrepancy - [ ] No balancing adjustment is ever created automatically - [ ] An account with import gaps is marked incompletely reconciled ### Watch 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. ## Phase 3 · Transfers, which are neither income nor spending Money moved between your own accounts stops appearing as both. ### Steps 1. Detect candidate pairs Same amount, opposite sign, two of your accounts, within a few days. Propose rather than assume. Files: `src/transfers.py` 2. Require confirmation A wrong automatic pairing is worse than an unpaired transfer, because it hides real spending. Files: `src/transfers.py` 3. Exclude from totals Confirmed transfers leave income and expense entirely. Verify totals change by the expected amount. Files: `src/reports.py` ### Done when - [ ] A genuine transfer is proposed and pairs on confirmation - [ ] An external payment is not proposed as a transfer - [ ] Confirming reduces both income and expense totals correctly - [ ] Unconfirmed candidates remain visible ### Watch 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. ## Phase 4 · Categories you can actually fix Spending is categorised by rules you wrote, and correcting one teaches the rule. ### Steps 1. Evaluate rules in order First match wins, with the matching rule recorded on the transaction so a wrong category is traceable. Files: `src/rules.py` 2. Turn corrections into rules Offer, do not assume. A one-off correction and a standing rule are different intentions. Files: `src/correct.py` 3. Make recategorisation explicit With the affected count shown before it runs. Files: `src/recategorise.py` ### Done when - [ ] Each transaction shows the rule that categorised it - [ ] A correction can create a rule that matches next month - [ ] Changing a rule leaves history alone until recategorisation - [ ] Uncategorised spending is visible in reports ### Watch 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. ## Phase 5 · Budgets and net worth You can see spending against a plan and what you own against what you owe. ### Steps 1. Compute budget actuals correctly Category spending with transfers excluded and uncategorised shown separately. Verify against a hand calculation. Files: `src/budgets.py` 2. Add manual assets and liabilities A property or a loan is not in any export and is most of net worth for many people. Files: `src/networth.py` 3. Leave gaps as gaps No interpolation. A missing month should be visible, since it means the data is incomplete. Files: `src/charts.py` ### Done when - [ ] A budget's actuals match a hand calculation - [ ] Net worth includes manual assets and updates on new balances - [ ] A month with no data appears as a gap - [ ] Changing a budget does not alter historical actuals ### Watch 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. ## Not in this build - 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. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The ledger. A complete record of your finances, so treat the file and its backups accordingly. DB_PATH=./data/ledger.db # Required. Reporting currency. Multi-currency accounts convert into it for totals. BASE_CURRENCY=GBP # Required. How far apart two sides of a transfer may be before they stop being proposed as a pair. TRANSFER_WINDOW_DAYS=4 # Required. Which aggregation provider, if any. The value none means file import only, which is a legitimate final state. AGGREGATOR=none # Optional · secret. Provider credential, stored outside the database and never committed. AGGREGATOR_TOKEN=replace-with-provider-token # Optional. How long without a refresh before an aggregation connection is reported as stale. STALE_CONNECTION_HOURS=48
# Monarch Money · product brief ## Problem A manual finance dashboard is buildable, but high-quality bank aggregation, transaction cleaning, investment data, household collaboration, and mobile sync make it real work. ## Product outcome A ledger whose totals reconcile to your statements, where transfers do not inflate spending, where categories are yours and correctable, and where a broken data connection tells you rather than quietly freezing your numbers. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - bank aggregation API or CSV import - secure database - categorization rules - charts - mobile/web app - backups ## Explicit non-goals for v1 - 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 ## Success criteria - No monetary value is stored as floating point - A deleted transaction is confirmed to produce a visible discrepancy - Transfer confirmation is confirmed to reduce both income and expense - An overlapping file and aggregation import is confirmed not to duplicate - An encrypted backup has been restored and reconciled
# Build brief · Monarch Money The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal finance tracker that categorises spending and shows net worth from data you actually control to replace Monarch Money. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Decide how transactions arrive before you build anything, because it determines cost, region and effort. Manual import from files your bank already exports always works, costs nothing and is the honest baseline. Automatic aggregation is a paid service in every case worth using, and the options differ sharply by region. As checked in September 2026: SimpleFIN Bridge is around 15 US dollars a year and is aimed at read-only personal finance with daily refresh. Teller offers a developer tier with a limited number of free live connections for United States banks. In Europe, Enable Banking offers free restricted production access limited to accounts you link yourself, while GoCardless was reported in 2025 to have stopped onboarding new bank data customers. Plaid's production access is sales-led. Verify current terms before committing, because this market moves. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with SQLite · the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows - Import: File parsers per institution, plus one optional aggregation provider · your bank's export always works and costs nothing, which makes it the baseline rather than the fallback - Money: Integer minor units, never floating point · a rounding error in a ledger is a bug that compounds silently and destroys trust in every total - Categorisation: Ordered rules you write, with corrections that create rules · someone else's categories are wrong about your spending and you cannot fix theirs - Reports: Hand-drawn SVG · a handful of charts with fixed requirements ### Data model (create this before Phase 1) - 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. ### Phase 1 · Importing without duplicating Build: Parse an export from one real institution into transactions with amounts in integer minor units. Deduplicate on a stable key. Record the import with the file hash so a re-import is recognised. Handle the awkward parts of real exports: date formats, sign conventions and encoding. Trap: 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. Trap: 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. Done when: A real export imports with correct amounts and dates. Importing the same file twice adds nothing and reports the duplicate count. A credit card export's signs are normalised so spending is negative and payments positive. No amount is stored as a floating point number, verified by inspecting the schema. Do not build yet: No categorisation, no reconciliation, no reports. ### Phase 2 · Reconciling to a statement Build: Record asserted balances from statements. Compute the balance from transactions and compare. Where they disagree, show the gap and the period it appeared in, rather than adjusting anything to make it match. Trap: 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. Done when: Computed and asserted balances match for a fully imported account. Deliberately deleting a transaction produces a visible discrepancy naming the amount and the period. No automatic adjustment is ever created. An account with a gap in its import history is marked as incompletely reconciled. Do not build yet: No categorisation or budgets yet. ### Phase 3 · Transfers, which are neither income nor spending Build: Detect pairs of transactions that are the same amount in opposite directions across two of your accounts within a few days. Propose them as transfers, require confirmation, and exclude confirmed transfers from income and expense totals. Trap: 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. Done when: A transfer between two imported accounts is proposed as a pair and, once confirmed, appears in neither income nor expense totals. A payment to an external account is not proposed as a transfer. Total income and total expense both fall by the transfer amount after confirmation. Unconfirmed candidates are visible rather than silently ignored. Do not build yet: No categorisation rules yet. ### Phase 4 · Categories you can actually fix Build: Ordered rules matching on description with an explicit uncategorised fallback. Correcting a transaction's category offers to create or amend a rule. Changing rules does not silently rewrite history; recategorising is an explicit action reporting how many transactions it will change. Trap: 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. Done when: A rule categorises matching transactions and the matching rule is shown on each. Correcting a category offers to create a rule and the offer produces one that matches the same merchant next month. Changing a rule leaves history alone until recategorisation is run. Uncategorised spending appears as its own line in every report. Do not build yet: No budgets or reports yet. ### Phase 5 · Budgets and net worth Build: Budgets per category per period, compared against actual spending with transfers excluded. Net worth from account balances including manually entered assets and liabilities. Show gaps in data rather than interpolating across them. Trap: 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. Done when: A budget compares against actual spending with transfers excluded, matching a hand calculation for one month. Net worth includes manually entered assets and updates when a balance is asserted. A month with no imported data appears as a gap rather than a straight line. Changing a budget does not alter historical actuals. Do not build yet: Aggregation and backups come last. ### Phase 6 · Automatic aggregation, if you want it Build: Add one aggregation provider chosen for your region, behind the same import interface the file parsers use. Store credentials outside the database. Handle the connection breaking, which it will. Then record the annual cost and what the provider can see. Trap: 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. Done when: Transactions arrive automatically and deduplicate correctly against previously imported files, verified by importing an overlapping period both ways. A broken connection produces an alert rather than silence. Credentials are outside the database. The annual cost and the provider's data access are written down. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable bank and investment sync across many institutions, which is the paid service and breaks constantly even when someone else is maintaining it. - Transaction categorisation trained on many users' corrections, which is why the commercial product is right more often on a merchant you have never seen. - Household collaboration with separate logins and shared visibility rules. - Investment performance analysis with holdings, cost basis and returns, which is a substantially larger project than a spending tracker. ### README must contain - That money is stored in integer minor units and never as floating point - That reconciliation shows discrepancies and never creates a balancing adjustment - That transfers must be paired or every category total is inflated - That changing a rule does not rewrite history until recategorisation is run explicitly - Which aggregation provider is used, what it costs annually, and what it can see
# Architecture · Monarch Money ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with SQLite | the work is parsing, reconciling and reporting, and the dataset is a few tens of thousands of rows | | Import | File parsers per institution, plus one optional aggregation provider | your bank's export always works and costs nothing, which makes it the baseline rather than the fallback | | Money | Integer minor units, never floating point | a rounding error in a ledger is a bug that compounds silently and destroys trust in every total | | Categorisation | Ordered rules you write, with corrections that create rules | someone else's categories are wrong about your spending and you cannot fix theirs | | Reports | Hand-drawn SVG | a handful of charts with fixed requirements | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Parsing sources into normalised transactions. | File parsers and aggregation share one interface, which is why adding a provider does not change deduplication or normalisation. | | Money | Integer minor units and formatting. | Not swappable for floating point under any circumstances. This is the invariant everything else rests on. | | Reconcile | Comparing computed and asserted balances. | Any implementation, provided it never adjusts to make numbers agree. | | Transfers | Pairing internal movements. | Detection can improve; requiring confirmation should not, because a wrong pairing hides real spending. | | Rules | Categorisation and its explanation. | Ordered rules are debuggable in a way a model is not. If you add a model, keep it proposing rather than deciding. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · The ledger. A complete record of your finances, so treat the file and its backups accordingly. - `BASE_CURRENCY` · required · Reporting currency. Multi-currency accounts convert into it for totals. - `TRANSFER_WINDOW_DAYS` · required · How far apart two sides of a transfer may be before they stop being proposed as a pair. - `AGGREGATOR` · required · Which aggregation provider, if any. The value none means file import only, which is a legitimate final state. - `AGGREGATOR_TOKEN` · optional, secret · Provider credential, stored outside the database and never committed. - `STALE_CONNECTION_HOURS` · optional · How long without a refresh before an aggregation connection is reported as stale. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Monarch Money product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with SQLite, File parsers per institution, plus one optional aggregation provider, Integer minor units, never floating point, Ordered rules you write, with corrections that create rules, Hand-drawn SVG. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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. - 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.
# Delivery milestones · Monarch Money Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Importing without duplicating You can import the same file twice and end up with one copy of each transaction. ### Steps 1. Parse one real export end to end Your own, not a sample. Real exports have inconsistent date formats and stray encoding. Files: `src/import/institution.py` 2. Store integer minor units Everywhere. Formatting happens at display and nowhere else. Files: `src/money.py` 3. Deduplicate on a stable key Date, amount, normalised description and account. Record the file hash so a re-import is detected. Files: `src/dedupe.py` ### Done when - [ ] A real export imports with correct amounts and dates - [ ] Re-importing the same file adds nothing and reports duplicates - [ ] Credit card signs are normalised correctly - [ ] No monetary value is stored as floating point ### Watch 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. ## M2 · Reconciling to a statement Your computed balance matches the bank's, or you can see exactly where it does not. ### Steps 1. Record asserted balances From statements, per account per date. These are the ground truth you reconcile against. Files: `src/balances.py` 2. Compare and localise the gap Compute running balance and find the first date where it diverges. That narrows the search. Files: `src/reconcile.py` 3. Never auto-adjust Show the discrepancy. Write a test that deletes a transaction and asserts the gap appears. Files: `tests/test_reconcile.py` ### Done when - [ ] Balances match for a fully imported account - [ ] A deleted transaction produces a visible discrepancy - [ ] No balancing adjustment is ever created automatically - [ ] An account with import gaps is marked incompletely reconciled ### Watch 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. ## M3 · Transfers, which are neither income nor spending Money moved between your own accounts stops appearing as both. ### Steps 1. Detect candidate pairs Same amount, opposite sign, two of your accounts, within a few days. Propose rather than assume. Files: `src/transfers.py` 2. Require confirmation A wrong automatic pairing is worse than an unpaired transfer, because it hides real spending. Files: `src/transfers.py` 3. Exclude from totals Confirmed transfers leave income and expense entirely. Verify totals change by the expected amount. Files: `src/reports.py` ### Done when - [ ] A genuine transfer is proposed and pairs on confirmation - [ ] An external payment is not proposed as a transfer - [ ] Confirming reduces both income and expense totals correctly - [ ] Unconfirmed candidates remain visible ### Watch 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. ## M4 · Categories you can actually fix Spending is categorised by rules you wrote, and correcting one teaches the rule. ### Steps 1. Evaluate rules in order First match wins, with the matching rule recorded on the transaction so a wrong category is traceable. Files: `src/rules.py` 2. Turn corrections into rules Offer, do not assume. A one-off correction and a standing rule are different intentions. Files: `src/correct.py` 3. Make recategorisation explicit With the affected count shown before it runs. Files: `src/recategorise.py` ### Done when - [ ] Each transaction shows the rule that categorised it - [ ] A correction can create a rule that matches next month - [ ] Changing a rule leaves history alone until recategorisation - [ ] Uncategorised spending is visible in reports ### Watch 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. ## M5 · Budgets and net worth You can see spending against a plan and what you own against what you owe. ### Steps 1. Compute budget actuals correctly Category spending with transfers excluded and uncategorised shown separately. Verify against a hand calculation. Files: `src/budgets.py` 2. Add manual assets and liabilities A property or a loan is not in any export and is most of net worth for many people. Files: `src/networth.py` 3. Leave gaps as gaps No interpolation. A missing month should be visible, since it means the data is incomplete. Files: `src/charts.py` ### Done when - [ ] A budget's actuals match a hand calculation - [ ] Net worth includes manual assets and updates on new balances - [ ] A month with no data appears as a gap - [ ] Changing a budget does not alter historical actuals ### Watch 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. ## M6 · Automatic aggregation, if you want it (production only) Transactions arrive without you exporting files, and you know what that costs and exposes. ### Steps 1. Add one provider behind the import interface The same deduplication and normalisation apply. Aggregation is a source, not a separate system. Files: `src/import/aggregator.py` 2. Test the overlap with file imports Import an overlapping period from both a file and the provider. Duplicates here are the failure mode. 3. Alert on a stale connection Expected refresh window per account. Silence is the dangerous state. Files: `src/health.py` ### Done when - [ ] Automatic and file imports of an overlapping period do not duplicate - [ ] A stale connection produces an alert - [ ] Credentials live outside the database - [ ] The annual cost and provider access are documented ### Watch 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.
# Operations · Monarch Money ## Backup The ledger is a complete financial record. Back it up encrypted, to a destination you control, and restore it once so you know the encryption key works. ## Restore Restore the database and confirm a recent month reconciles against a statement. That check is what proves the restore rather than the file merely opening. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Aggregation connection freshness and reconciliation discrepancies. A growing discrepancy usually means an import gap rather than an arithmetic bug, and it is worth catching before you rely on a report. ## Incident checklist If totals look wrong, check transfer pairing before anything else, since unpaired transfers are the most common cause. If a connection stopped, the alert should already have fired; if it did not, that is the first bug to fix. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No monetary value is stored as floating point - [ ] A deleted transaction is confirmed to produce a visible discrepancy - [ ] Transfer confirmation is confirmed to reduce both income and expense - [ ] An overlapping file and aggregation import is confirmed not to duplicate - [ ] An encrypted backup has been restored and reconciled ## Launch constraint Do not market omitted Monarch Money capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The ledger. A complete record of your finances, so treat the file and its backups accordingly. DB_PATH=./data/ledger.db # Required. Reporting currency. Multi-currency accounts convert into it for totals. BASE_CURRENCY=GBP # Required. How far apart two sides of a transfer may be before they stop being proposed as a pair. TRANSFER_WINDOW_DAYS=4 # Required. Which aggregation provider, if any. The value none means file import only, which is a legitimate final state. AGGREGATOR=none # Optional · secret. Provider credential, stored outside the database and never committed. AGGREGATOR_TOKEN=replace-with-provider-token # Optional. How long without a refresh before an aggregation connection is reported as stale. STALE_CONNECTION_HOURS=48
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because personal finance apps are only valuable when data arrives automatically and safely.
xbank/investment sync
xtransaction categorization
xhousehold collaboration
xmobile apps
xsupport
xdata security posture
Don't feel like building it? These folks already made it free.
all 4 free alternatives to Monarch Money →· no votes, no pay-to-list · just what's real
Monarch Money pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| core | $14.99 | $8.33 | One household; budgeting, goals, cash flow, net worth and account tracking |
| plus | — | $16.58 | Everything in Core plus forecasting, business tracking, advanced investment analysis, gains/losses and estate-planning features |
free tierno free tier; 7-day free trial
billingCore monthly + annual; Plus annual only
hidden costsTaxes may apply; app-store billing terms can differ. Core promotions do not apply to Plus or monthly plans. Plus is unavailable on advisor-sponsored accounts.
verified 2026-08-13 · source ↗
Vibecode Monarch Money
Kinda. The core of Monarch Money is buildable in a weekend with the prompt on this page, but there are real gaps: bank/investment sync, transaction categorization. Read the honest list above before committing.
How much does Monarch Money cost?
Monarch Money costs about $14.99/month (Core, checked 2026-07-30), which is $179.88 per year.
What do I lose by replacing Monarch Money?
Honestly: bank/investment sync; transaction categorization; household collaboration; mobile apps; support; data security posture. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Monarch Money?
Yes: Actual Budget (Budgets, imported transactions, schedules and reports on your disk; investment analytics are much thinner.) Firefly III (Transactions, budgets, goals and net worth in Docker; portfolio analysis is not its favourite spreadsheet.) Wealthfolio (Investments, net worth, spending, budgets and goals on your device; automatic broker sync is the paid shortcut.) All 4 curated free alternatives are at vibecodeit.com/monarch-money/alternatives. The prompt is for when you want it exactly your way.