← All work

Case study

Agentic wealth operations

Self-hosted personal-finance automation under a written operations contract: cost-tiered updater agents over MCP, human-gated imports and reconciliation, a ledger designed so tax season and life-to-date questions become filter operations, and every statement sanitized on the operator's own machine before any model reads it.Personal-finance software running on my own home server, kept up to date by AI helpers working under a written rulebook: every change is approved by a human, the books are organised so tax season and lifetime questions become simple look-ups, and every bank statement is scrubbed of identifying details on my own computer before any AI reads it.

Maturity: Operational
MaturityOperational
TopicsAgent orchestration · Workflow governance · Privacy
TechnologiesFirefly III · Ghostfolio · Model Context Protocol · Cost-tiered agent skills · Docker Compose
Reading levelSame figures at both levels.

Problem

Personal finance kept failing in one of two ways: either the ledgers went stale because entry was manual, or keeping them current consumed evenings. Reconciliation was a seasonal project, and simple questions — what have home expenses actually run this year? — meant a spreadsheet session rather than an answer. The goal was a system where the ledgers stay current at trivial cost, questions get answered when asked, and year-end tax preparation is a filter operation instead of archaeology — without any cloud finance service ever seeing the data.

Keeping track of household money kept failing in one of two ways: either the record books fell out of date because every entry had to be typed in by hand, or keeping them current ate my evenings. Checking the books against bank statements was a once-a-season chore, and simple questions — what has running the house actually cost this year? — meant an evening in a spreadsheet rather than an answer. I wanted the books to stay current with almost no effort, questions answered when I ask them, and tax season reduced to looking things up instead of digging things out — all without ever handing my financial data to an online service.

Constraints

In plain terms first: everything runs on computers in my own home, the AI helpers only do routine record-keeping — never advice — and they can change nothing without me approving that specific change. The precise ground rules:

  • Everything self-hosted; financial data never leaves the home network, and platform API tokens are held client-side only — never inside shared services.
  • Agents perform mechanical execution only: completed-activity entry and import preparation. Investment advice, allocation decisions, and account architecture are explicitly out of scope.
  • Source systems are read-only to agents by default; every write is individually authorized by the operator, read back after writing, and compared against intent.
  • An agent stops rather than guesses — on account, symbol, amount, direction, category, transfer counterpart, or tax treatment.
  • Nothing an assistant reads is a raw statement. Bank and brokerage documents are sanitized on the operator’s own machine first — identifiers replaced with stable pseudonyms from a names file, the PDF container discarded, the output re-scanned — because a model needs dates, amounts, and merchant names to categorise a transaction, and none of the rest. What leaves the network is text with the identifiers already gone; the mapping back to real identifiers stays on the operator’s side.
  • Public descriptions of this system, this page included, carry no amounts, balances, institutions, account identifiers, or tax figures by design. Every figure in the dashboard screenshot below is synthetic, generated for illustration.

Architecture

Two self-hosted open-source platforms split the domain. Firefly III — a double-entry bookkeeping system for personal finances — owns transactions and the data importer; Ghostfolio — a wealth-management platform for investment holdings — owns portfolios and accounts. Each runs on the home-lab Docker stack with its own database, scheduled backups, and admin tooling behind a reverse proxy. Neither is treated as the retirement-planning source of truth — that separation is written down, not assumed.

Two free, openly developed programs share the work, both running on my own home server. Firefly III keeps the day-to-day money records — every transaction in and out. Ghostfolio keeps track of investments — what is held and where. Each has its own database and its own scheduled backups. Neither one is treated as the master source for retirement planning — that rule is written down, not assumed.

Statement pipeline and agent access under the operations contract Bank and card exports flow into agent-prepared import files, which pass human operator review before reaching the Firefly III transaction ledger through the data importer. The operator also authorizes writes down to the cost-tiered updater agents, which work through MCP servers whose tokens stay client-side, reaching both Firefly III and Ghostfolio. The agents and MCP servers sit inside a fence labelled operations contract: read-only default, per-write authorization, read-back verification. Statement exports bank & card files Prepared-import files agent-prepared · held for review Operator review human sign-off Firefly III transaction ledger Ghostfolio holdings & accounts authorizes writes Operations contract read-only default · per-write authorization · read-back verification Updater agents cost-tiered · tool-allowlisted MCP servers tokens stay client-side

Around the platforms sits the operational layer that keeps the whole thing trustworthy: a version-pinning policy and container-update triage for every image, compose drift audits, and a verified database major-version upgrade performed with row-count reconciliation before cutover. A read-only reporting database and BI stack provide a heavier analysis path without touching the live ledger, and a Gitea wiki serves as the workstream’s system of record — a director-status roll-up, intake-readiness state, and a card for every agent with its safety rules.

Around those two programs sits the routine upkeep that keeps the whole thing trustworthy: software updates are reviewed before they are applied rather than installed blindly, and when the main database needed a major upgrade, row counts were checked to confirm nothing was lost before switching over. Heavier analysis happens on a separate read-only copy of the data, so the live books cannot be damaged by it. And how the system is run is itself written down in a private wiki — including a card for every AI helper with its safety rules.

AI and agent workflow

Routine data entry belongs to two bounded specialist agents, deliberately cost-tiered:

  • Transaction updater — stronger model, explicit ten-tool MCP allowlist.
  • Holdings updater — cheaper model, because its work is simpler.
  • One canonical skill body shared by two agent runtimes — one contract to maintain.
  • A cached account snapshot with a seven-day freshness rule — routine entry avoids re-downloading data it already has.

Each runs a published procedure. The transaction updater begins with a gathering run that measures how far behind the ledger actually is — from the last statement-derived row per account, not the last activity, which workflow postings keep artificially current — and emits a checklist of statements to download; the import that follows validates statement continuity, maps rows deterministically, holds the ambiguous, and gives every row a stable identity. The holdings updater takes a pasted fill or dividend notification, resolves the symbol against what is actually held, validates the arithmetic, checks for duplicates, and shows the proposed record before anything is written.

Routine record-keeping is handled by two specialist AI helpers — “agents” — each with a deliberately limited job:

  • Transaction updater — records day-to-day money movements. It runs on a stronger AI model, and it is limited to a fixed list of exactly ten actions it is allowed to take — nothing else.
  • Holdings updater — records investment updates. Its work is simpler, so it runs on a cheaper model.
  • Both helpers follow one shared instruction sheet, so there is only one set of rules to maintain.
  • A saved copy of account details, refreshed on a seven-day rule, means routine entries don’t re-download information the system already has.

Each follows a published step-by-step procedure. The transaction updater starts by working out how far behind the books really are — measured from the last entry that came from a bank statement, not the last entry of any kind, since automated postings make the books look more current than they are — and produces a checklist of statements to download. The import that follows checks that the statements join up without gaps, maps every row by fixed rules, sets aside anything unclear, and gives every row a permanent identity. The investment helper takes a pasted broker notification of a trade or dividend, matches the security against what is actually held, checks the arithmetic, checks for duplicates, and shows the proposed entry before anything is written.

The MCP layer has a build-then-adopt history:

  • When no trustworthy server existed for the ledger, I built one: read-only, behind an egress-guard container enforcing GET/HEAD-only traffic against path and query allowlists, with tests and rollback docs.
  • When maintained community servers caught up, I retired it in their favour.
  • A small REST script covers the one write the holdings MCP schema can’t express, verified by read-back.

The helpers talk to the two programs through connectors — the technical name is MCP, a standard way for AI tools to work with other software. The connector story matters:

  • When no trustworthy connector existed for the money records, I built one myself: it could only read, never write, and it sat behind a guard that blocked every kind of request except safe read-only ones — with tests, and instructions for undoing it.
  • When well-maintained community connectors caught up, I retired mine in their favour.
  • One small script covers the single kind of update the investment connector cannot express; after it writes, the result is read back and checked.

The taxonomy is the part that does the most work, and it is now public in generic-label form:

  • Two tag axes rather than one pile: a cost-centre axis the accountant filters on, and a behaviour axis — fixed, variable, subscription — that budgets and subscription tracking depend on; plus a year tag and an import-cycle tag on every imported row.
  • Credit cards as asset accounts with negative balances, so a card payment is a transfer between two owned accounts and reconciles against the statement.
  • Broker accounts as cash mirrors only — the transfer in and out is recorded, the securities are not; holdings and performance stay in Ghostfolio.
  • A clearing account for charges the statement shows as one line — a marketplace order, a day of rideshare legs — so its balance is exactly the un-itemised spend: one honest health number.

The way entries are organised does the most work, and that structure is now public with every institution replaced by a generic label:

  • Two kinds of label rather than one pile: one says whose cost it is — personal, home office, business travel — which is what an accountant filters on; the other says how it behaves — fixed, variable, subscription — which is what budgets and subscription tracking rely on. Every imported row also carries a year label and a label naming the statement it came from.
  • A credit card is recorded as an account with a negative balance, so paying it off is a transfer between two accounts I own, and the payment matches the statement line by line.
  • Broker accounts appear only as cash in and cash out — the money moved, not the shares bought; what is held, and how it has performed, lives in Ghostfolio.
  • One holding account catches charges that a statement shows as a single line but that are really several purchases — an online marketplace order, a day of rideshare trips. Its balance is exactly the spending I have not yet itemised: one honest health number.

The same taxonomy powers questions:

  • Spend answers in conversation — what have home expenses cost?
  • What-if scenarios — run a rental property as its own cost centre, and what would it cost life-to-date?
  • When a question outgrows a chat answer it becomes an artifact: a tested mortgage-equity engine (figures in config, none in code) renders a single-file HTML dashboard, kept as a living report.

Because every entry is filed under a well-designed set of categories, questions become easy:

  • Everyday answers in conversation — what has running the house cost?
  • What-if questions — if a rental property were tracked as its own cost centre, what would it have cost over the years?
  • When a question outgrows a chat answer it becomes its own small tool: a tested mortgage calculator (every personal figure lives in a private settings file, none in the code) that renders a one-page dashboard, kept as a living report.
Single-page mortgage dashboard with summary tiles for balance and equity, a payoff-projection chart, and a per-party contribution table, rendered from synthetic example data
The mortgage dashboard, rendered by the same code that produces the private version — but from synthetic figures. Nothing shown is a real amount, rate, date, or party.

Human governance

A written operations contract governs every agent action — the part of this system I would rebuild first anywhere else:

A written rulebook — the operations contract — governs everything the AI helpers do. It is the part of this system I would rebuild first anywhere else:

  • Mechanical execution only; agents never advise.
  • Systems are read-only until the operator authorizes a specific write.
  • Every write follows one discipline: resolve → validate → state the exact operation → write → read back and compare → report with a provenance stamp.
  • Stop conditions replace judgment calls: any ambiguity in account, amount, direction, category, or tax treatment ends the attempt.
  • Anything irreversible goes back to the operator as a bounded manual runbook.

Reconciliation stays human: agents prepare the matching, the operator signs the books. Statement exports become premapped entries plus held-for-review files — nothing reaches the ledger without operator review.

Checking the books against the bank’s records stays a human job: the helpers line everything up, and I sign off. Bank statement files are turned into ready-to-review entries — nothing reaches the books until I have looked at it.

Nothing leaves the network unsanitized

The privacy constraint has its own tooling, published with its tests in the ledger repository. A statement PDF never reaches a cloud model. The sanitizer extracts the text, replaces every identifier with a stable pseudonym — email, account and card numbers, national identifier, phone, postal code, street address, any long digit run, and every term in an operator-maintained names file — discards the PDF container, and writes plain text plus a redaction report. Dates, amounts, merchant names, and descriptions survive, so the output is still analysable; and because pseudonyms are stable within a run, a transfer between two accounts still reads as a transfer between two distinct accounts.

Three design decisions carry the guarantee:

  • Never redact in place. A PDF with black boxes drawn on it still holds the original text in its content stream, its metadata, and its annotations. The script never writes a PDF.
  • The names file is the part patterns cannot do. Regexes catch account numbers; only the operator can tell the script that a particular name, employer, or street is theirs. Running without one is treated as a failure for anything meant to be shared.
  • Trust the report, not the script. A strict mode re-scans the output and exits non-zero if any high-risk pattern survived, the report lists exactly what was replaced, and the operator reads the sanitized text before it goes anywhere. When an identifier must travel back into the ledger — an import file, an MCP write — the mapping happens on the operator’s side, never in the assistant’s context.

The same discipline governs what this page shows: the screenshot tour in the repositories rewrites the rendered page before every capture and refuses to write a file if a single original term survives.

The privacy rule has its own tool, published with its tests alongside the money-records setup guide. A bank or broker statement never reaches an online AI service as it is. The scrubbing tool pulls the text out of the PDF, replaces every identifying detail with a stand-in label — email addresses, account and card numbers, government ID numbers, phone numbers, postal codes, street addresses, any long run of digits, and every name on a list I maintain — throws the PDF away, and writes plain text plus a report of what it replaced. Dates, amounts, shop names and descriptions stay, so the result is still useful; and because the same account number always gets the same stand-in, a transfer between two of my accounts still reads as a transfer between two different accounts.

Three decisions make that hold:

  • Never black out a PDF and call it done. A PDF with black boxes drawn on it still contains the original text underneath, in its hidden properties and its notes. The tool never produces a PDF at all.
  • The names list is the part a pattern cannot do. A pattern can spot an account number; only I can tell the tool that a particular name, employer or street is mine. Running without that list counts as a failure for anything I intend to share.
  • Trust the report, not the tool. A strict mode re-checks the output and refuses to pass if anything risky survived, the report lists exactly what was replaced, and I read the scrubbed text before it goes anywhere. When a real identifier has to go back into the books — an import file, a write through the connector — that translation happens on my side, never inside the AI’s conversation.

The same discipline governs what this page shows: the screenshot tool in the setup guides rewrites the page before every capture and refuses to save if a single original term survives.

Evidence

The work is evidenced in two layers. The setup path is public; the household data behind it is private by design.

Public — two MIT-licensed setup repositories, every figure in them synthetic:

  • firefly-nas-setup — compose stacks for the ledger, its importer, the cron caller, and the MCP server; the chart of accounts, subscription and budget design, and itemisation pipelines in generic-label form; the statement sanitizer with its tests; the gathering and import procedures as agent definitions and as a system prompt; and a screenshot tool that rewrites the rendered page before capture and refuses to write a file if any original term survives.
  • ghostfolio-nas-setup — compose stacks for the holdings platform and its MCP sidecar, a comparison of the built-in and community MCP routes, a synthetic sample portfolio priced from public market data, the activity-recording procedure with its acceptance cases, and a sanitized screenshot tour.

Private:

  • The workstream repository — operations contract, canonical skills, agent definitions with model pins and tool allowlists, tested analysis code.
  • A twenty-four-issue delivery history — what shipped and what failed, including an import dedupe trap found the hard way.
  • The wiki — agent cards and status roll-ups.

The proof of this work comes in two layers. How it is set up is public; the household data behind it stays private on purpose.

Public — two open-source setup guides on GitHub, with every figure in them invented:

  • firefly-nas-setup — the files that run the money-records program, its importer, its daily scheduler and its AI connector; the account structure, budget and subscription design, and the receipt-matching pipelines with every institution replaced by a generic label; the statement-scrubbing tool with its tests; the helpers’ step-by-step procedures; and a screenshot tool that rewrites the page before it captures anything and refuses to save if a single original term survives.
  • ghostfolio-nas-setup — the files that run the investment program and its AI connector, a comparison of the two ways to connect an assistant to it, a made-up sample portfolio priced from public market data, the trade-recording procedure with its test cases, and a screenshot tour.

Private:

  • The project’s own repository — the rulebook, the helpers’ instruction sheets and settings, and tested analysis code.
  • A delivery history of twenty-four tracked work items — what shipped and what failed, including a duplicate-import trap found the hard way.
  • The private wiki — a card for every helper, and regular status summaries.

This page describes capabilities only; figures, institutions, and identifiers stay private, which is itself one of the system’s constraints.

Maturity, stated plainly:

  • Operational, artifact-verified — the platforms, updater agents, operations contract, and mortgage dashboard. The setup path and the agent procedures are additionally public, in sanitized form, in the two repositories above.
  • Design and delivery-record level — the statement pipeline.
  • Owner-attested — that I ask spend questions routinely, and that year-end tax packages get assembled from the schedule-aware tags. The enabling architecture is verifiable; the habits are not.

Outcome

The ledgers stay current at low cost, with routine updates delegated to the cheapest model that does the job correctly. Statement-import season became a pipeline with a human gate. Spend questions get answered in conversation instead of in spreadsheets. Tax-schedule-aware tagging — each category mapped to its return treatment — turns year-end preparation into a filter operation over clean data. Both ledgers are now independent of any vendor’s interface: a broker can reset its performance display or a bank change its export format, and the history stays queryable in one place under categories the operator chose. None of this constitutes tax or investment advice; it is one operator’s tooling for their own books, built to be inspectable.

The books now stay current with very little effort, and the routine updating is done by the cheapest AI model that does the job correctly. Statement-import season became an assembly line with a human checkpoint. Money questions get answered in conversation instead of in spreadsheets. And because every category is labelled with how it is treated on a tax return, year-end preparation means filtering clean records rather than reconstructing a year after the fact. Both sets of records now stand on their own: a broker can change how it shows performance, or a bank change its download format, and the history is still there in one place, under categories I chose. None of this constitutes tax or investment advice; it is one operator’s tooling for their own books, built to be inspectable.

Lessons

  • Build-versus-buy is a sequence, not a stance: build the gated version when nothing trustworthy exists, adopt the maintained one when it matures, and keep only the small script that fills the remaining gap.
  • Tier model cost by task, not by habit — the holdings updater lost nothing by moving to a cheaper model, and the split is where the “cost-effective” in cost-effective agents actually comes from.
  • A written operations contract beats per-session judgment. Stop conditions that preserve uncertainty — stop rather than guess — are what make agent writes to financial records tolerable at all.
  • Design the taxonomy for the questions you will ask later. Tags added at entry time cost nothing; the same semantics reconstructed at year-end is archaeology.
  • A feature that depends on a scheduler nobody configured fails silently. Firefly III has no internal clock: auto-budget limits, recurring transactions, and bill warnings fire only when something calls its cron endpoint daily. The reference install ran five months with budgets configured and no limit ever created; the cron caller is now part of the compose stack.
  • Make the unclassified visible as one number. Routing multi-item charges through a clearing account turns “how much have I not itemised?” into a balance — about a tenth of a year’s spend was sitting there before the pipeline existed.
  • Sanitize on your own machine, before the model, with a names file. A cloud model runs on someone else’s hardware under retention terms nobody read; the analysis needs dates, amounts, and merchant names and none of the identifiers, so the leak has no upside. Black boxes on a PDF are not redaction.
  • “Build it yourself or use someone else’s” is a sequence, not a one-time choice: build a locked-down version when nothing trustworthy exists, switch to the well-maintained community one once it matures, and keep only the small piece that still has no substitute.
  • Match the cost of the AI model to the difficulty of the task, not to habit — the investment helper lost nothing by moving to a cheaper model, and that split is where the real cost saving comes from.
  • A written rulebook beats deciding things on the fly. The rule that a helper must stop rather than guess is what makes letting software touch financial records tolerable at all.
  • Design your categories around the questions you will want answered later. A label added when a record is filed costs nothing; recreating that meaning at year-end is archaeology.
  • A feature that relies on a timer nobody set up fails silently. Firefly III has no internal clock: budget limits, repeating transactions and bill reminders only happen when something calls its daily trigger. Mine ran for five months with budgets set up and not one limit ever created; the daily trigger is now part of the standard setup.
  • Make what you haven’t classified visible as one number. Sending multi-item charges through a holding account turns “how much have I not sorted yet?” into a single balance — about a tenth of a year’s spending was sitting there before that pipeline existed.
  • Scrub the document on your own computer, before any AI sees it, with your own list of names. An online AI service runs on someone else’s hardware under terms nobody read; the job needs dates, amounts and shop names and none of the identifying details, so sending them has no upside. Black boxes on a PDF are not redaction.