Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Personal Finance AI Coach

The brief

The usual phrasing is deceptively small:

“Design an AI-powered personal finance app. It connects to a user’s bank accounts, categorizes their transactions, flags unusual spending, and gives them personalized budgeting advice.”

Or the version that tells you the interviewer has thought about it:

“Build the thing Mint should have become. Ten million users, connected bank accounts, and a coaching layer that actually changes behaviour. How do you build it and what keeps you up at night?”

In plain terms, the product pulls a user’s transactions from their banks. It sorts them into categories. It works out what normal looks like for that person. It notices when something is off, and it talks to them about it. The talking part is what people call the AI. The categorising, the noticing, and above all the not-getting-sued are the actual product.

Two things are worth saying in the first two minutes. First, the highest-volume machine learning task here is transaction categorisation. That is a text classification problem, and a small model solves it better and roughly a thousand times cheaper than an LLM. Second, the words “budgeting advice” carry regulatory weight. A candidate who notices that before being prompted is immediately in a different bracket.


What I’d ask first

Is this advice or information? This is the first question, and it reshapes everything downstream. “You spent $840 on restaurants this month, 30% above your six-month average” is information about the user’s own data. “You should move your emergency fund into a high-yield savings account” is guidance about a financial product. Depending on jurisdiction and framing, that can put you in the territory of regulated advice. “Sell your Tesla position” is investment advice, and it requires registration. Where the interviewer puts this line determines whether the product is a dashboard with a chat interface or a regulated entity.

Do we hold money, or only read data? A read-only aggregation product and a product that moves funds are separated by an enormous compliance gap. That gap includes money transmitter licensing and KYC/AML obligations. Assume read-only unless told otherwise, and say why.

How do we get bank data, and who owns the connection? The options are direct APIs, an aggregator such as Plaid or MX, or screen scraping. The choice determines your data quality, your latency, your per-user cost, and your exposure to a category of failure entirely outside your control. It also determines what happens the week a large bank changes its authentication flow.

Which markets? US, UK/EU, or both. UK and EU open banking is a mandated, standardised, consent-driven API regime. The US is a commercial aggregator market. Its regulatory framework is the CFPB’s Section 1033 rule, which as of 2026 is enjoined and under reconsideration by the Bureau, so you cannot build a roadmap on its deadlines (status summary, CFPB reconsideration page). That uncertainty is itself a design input. You build for a world where bank data access may cost money and may change terms.

What’s the business model? This is not an idle question, because it determines what you are allowed to do with the data. Subscription means the user is the customer, so you can promise not to monetise their transactions. Lead generation for financial products means you have a conflict of interest to disclose, and it changes the guardrails on what the coach says. Selling anonymised spending data to hedge funds is a real business in this space, and it is incompatible with several of the privacy promises below.

How many users, and what’s the transaction volume? Ten million users at roughly forty transactions a month is 400 million categorisation calls a month. At LLM prices that is a line item that kills the company. At small-model prices it is rounding error. Getting the interviewer to state the volume is how you earn the right to make that argument concretely.

What I’ll design against

Consumer app, US and UK, five million users, read-only account aggregation via an aggregator, with direct bank APIs where available. Roughly 200 million transactions a month. Subscription revenue, no data sales, and no product referrals in v1. Coaching is explicitly informational and educational. It covers budgeting, spending patterns, and cash-flow forecasting, with a hard line against securities, tax, and credit advice. No money movement.


The design

   BANK CONNECTIVITY              INGESTION & NORMALIZATION
 ┌──────────────────┐        ┌──────────────────────────────┐
 │ Open Banking API │        │  webhook / poll ingester     │
 │ (UK/EU, consent) ├───────▶│  · dedupe by external id     │
 ├──────────────────┤        │  · pending → posted merge    │
 │ Aggregator       ├───────▶│  · currency + sign normalize │
 │ (Plaid/MX, US)   │        │  · merchant string cleanup   │
 ├──────────────────┤        └──────────────┬───────────────┘
 │ manual CSV / OFX ├───────▶               │
 └──────────────────┘                       ▼
                              ┌──────────────────────────────┐
                              │ CATEGORIZATION CASCADE       │
                              │  1. user override (sticky)   │
                              │  2. merchant lookup table    │
                              │  3. deterministic rules      │
                              │  4. small text classifier    │
                              │  5. LLM  ← only the tail     │
                              └──────────────┬───────────────┘
                                             ▼
      ┌──────────────────────────────────────────────────────────┐
      │ ENCRYPTED TRANSACTION STORE (per-user key, row-level)    │
      │  transactions · accounts · balances · categories         │
      └───────┬──────────────────────┬───────────────────┬───────┘
              │                      │                   │
              ▼                      ▼                   ▼
   ┌────────────────────┐  ┌──────────────────┐  ┌────────────────────┐
   │ ANALYTICS ENGINE   │  │ ANOMALY DETECTOR │  │ MEMORY / PROFILE   │
   │ budgets, trends,   │  │ per-user, per-   │  │ goals, prefs,      │
   │ recurring detect,  │  │ category robust  │  │ prior nudges,      │
   │ cash-flow forecast │  │ z-score + rules  │  │ what worked        │
   └─────────┬──────────┘  └────────┬─────────┘  └─────────┬──────────┘
             └──────────────┬───────┴──────────────────────┘
                            ▼
              ┌───────────────────────────────┐
              │ COACH (LLM)                   │
              │ · reads computed facts ONLY   │
              │ · never does arithmetic       │
              │ · scope + disclaimer guardrail│
              │ · output classifier on egress │
              └──────────────┬────────────────┘
                             ▼
              ┌───────────────────────────────┐
              │ APP: feed, chat, budgets,     │
              │ notifications, consent centre │
              └───────────────────────────────┘

Bank connectivity. There are three paths, and you will support all three. In the UK and EU, open banking APIs give you consented, standardised access with a legally mandated consent lifecycle. Consent expires and must be re-authorised, typically every 90 days. That re-consent flow is a serious retention problem you must design for, not a footnote. In the US you go through an aggregator, and increasingly through direct bank APIs where the aggregator has them, with screen scraping as a decaying fallback. Manual CSV or OFX import covers the long tail of institutions nobody supports.

The engineering reality of this layer is that it is the single largest source of production incidents in the product, and it is entirely outside your control. Connections break when a bank changes its login flow, when MFA is enforced, when consent expires, and when the aggregator has an outage. So design for it. You need a connection health model per link, background re-auth prompts that are not annoying, graceful degradation to last-known data with an honest staleness indicator, and a reconciliation job that detects gaps in transaction history rather than silently showing an incomplete picture.

Ingestion and normalisation. Deduplicate on the provider’s external ID, because the same transaction arrives repeatedly. A duplicate $2,000 rent payment in someone’s budget is a support ticket and a trust event. Merge pending into posted. The amount can change, because of a restaurant tip or a fuel pump pre-auth, and the merchant string usually changes too. Normalise currency and sign conventions, which differ per institution in ways that will surprise you. Clean merchant descriptors, so SQ *BLUE BOTTLE 4471 OAK becomes Blue Bottle Coffee. That is a string-processing problem with a big lookup table, not an AI problem.

Categorisation cascade. This is the heart of the system. It is deliberately layered so that almost nothing reaches the expensive tier:

  1. User override. If this user has ever recategorised this merchant, that wins, permanently and instantly. Nothing else in the system may overrule it. This single rule handles the majority of user-perceived accuracy complaints.
  2. Merchant lookup. A curated table maps normalised merchant identity to category. Amazon is ambiguous. Starbucks is not. This resolves the large majority of volume with zero inference.
  3. Deterministic rules. MCC codes, meaning the merchant category code carried on card transactions. Also transfer detection between the user’s own accounts, and recurring-payment matching.
  4. Small supervised classifier. A fine-tuned small text model handles the residual. Honestly, a well-tuned gradient-boosted or linear model over character n-grams of the descriptor, plus amount and MCC features, works just as well. It runs in single-digit milliseconds, costs effectively nothing, and retrains nightly on user corrections.
  5. LLM. Use it only for genuinely novel merchants the classifier is unconfident about. Write the result back into the merchant table, so the same string is never sent twice.

Storage. Transaction data is among the most sensitive personal data that exists. So use encryption at rest with per-user keys, strict row-level access control, and field-level encryption on account numbers. Add a hard rule that raw credentials never touch your systems. Credentials are the aggregator’s job, and that is one of the main reasons to use one. Define the retention policy up front, with deletion that actually deletes, including from backups and analytics stores, because a user in the EU or California will ask.

Analytics engine. This is deterministic, tested, boring code. It covers budget calculation, month-over-month trends, recurring subscription detection (periodicity plus amount stability plus merchant match), cash-flow forecasting from recurring income and obligations, and category rollups. Every number the user ever sees is produced here.

Anomaly detection. Per user, per category, and mostly statistics. Use a robust z-score against that user’s own trailing distribution, computed with median and MAD rather than mean and standard deviation, because one $4,000 transaction destroys a mean. Add seasonality awareness, because December is not November. Add first-time-large-merchant rules, duplicate-charge detection, and a subscription price-increase detector. The last two are the ones users actually thank you for.

Memory and personalisation. Keep a structured user profile rather than a pile of chat transcripts. The profile holds stated goals, income pattern, fixed obligations, risk of overdraft, communication preferences, which nudges they engaged with, which they dismissed, and what they explicitly told the coach to stop mentioning. Retrieval over past conversations gives extra context. However, the profile is the durable object, and it is inspectable and editable by the user.

The coach. This is an LLM with a tightly bounded job. It takes computed facts and turns them into something a person will read and act on. It receives a structured payload of numbers it did not compute. It does not have a calculator, and it is not asked to be one. It has scope constraints enforced both in the prompt and by a classifier on the way out.


Where the AI actually is

Categorisation is not an LLM job. Run the numbers out loud in the interview, because that settles the argument. 200 million transactions a month through an LLM, even a cheap one at a tenth of a cent each, is $200,000 a month. A 5MB classifier does the same task at higher accuracy. The accuracy is higher because the classifier is trained on your users’ actual corrections. It learns that AMZN MKTP from this particular user is usually household supplies. An LLM has broad world knowledge and no knowledge of your data distribution. The LLM is also slower and non-deterministic, and it will occasionally invent a category that is not in your taxonomy. The correct architecture uses it for perhaps 1% of volume, in the tail, with the result cached forever.

Anomaly detection is statistics. “This is 3.2 MADs above your trailing six-month median for this category” is a defensible, explainable, cheap statement. Asking a language model whether a spending pattern is unusual produces something that sounds insightful and is not grounded in anything. You cannot tune its sensitivity, you cannot explain a given firing to a user, and you cannot regression-test it.

Never let the model do arithmetic. This is the hardest rule in the chapter, and it is the one most often broken. Every figure comes from the analytics engine as a value in a structured payload. That means every total, average, percentage, projection, and balance. The model’s job is to select which of those facts to mention, and to phrase them. If a number appears in the output that is not in the payload, that is a bug. You should be able to detect it programmatically by validating generated numerals against the input set before display. A finance app that states a wrong number has failed at the only thing it is for.

Where the model genuinely earns its place:

  • Explanation and framing. Turning six computed facts into three sentences that land, with the right tone for a user who is stressed about money.
  • Conversation. “Why was last month so expensive?” is a question whose answer requires assembling several computed views and narrating the comparison. That is genuinely a language task.
  • Goal decomposition. “I want to save £5,000 for a deposit by next June” becomes a structured plan with monthly targets and the categories where there is realistic slack. All the arithmetic is done in code, and the model does the structuring and the persuading.
  • Merchant disambiguation in the tail, as above.
  • Behavioural nudging. The gap between a fact and a behaviour change is writing, and writing is what these models are for.

Here is the honest split again. The model work is a prompt, an output guardrail, and an eval set, so a few weeks. The years go to bank connectivity, connection health, dedupe and reconciliation, the merchant table, encryption and key management, consent lifecycle, deletion, notification infrastructure, and the mobile app.


Key decisions and tradeoffs

ForkCase for ACase for BWhat I’d do
Aggregator vs direct bank integrationsAggregator gives thousands of institutions on day one, and handles credential security and auth flowsDirect is cheaper at scale, more reliable, better data, and has no middleman who can reprice you. Banks have started charging aggregators for accessAggregator to launch. Add direct integrations for the top institutions by user count once volume justifies it. Abstract the provider behind one interface from day one, so this is a migration and not a rewrite
Global category taxonomy vs per-user categoriesGlobal enables benchmarking, aggregate insight, and a single modelUsers think in their own terms and will fight your taxonomyA fixed global taxonomy underneath, with user-defined labels and rules mapping onto it. Never let a custom label break the analytics
Cloud LLM vs self-hosted for the coachCloud gives the best model quality, no infrastructure, and fast iterationSending transaction data to a third party is a privacy and contractual question users care about, and it is a compliance conversation in the EUCloud with a zero-retention enterprise agreement and aggressive minimisation. Send computed facts and category names, not raw merchant strings or account identifiers. Revisit if enterprise or EU customers demand residency
Proactive nudges vs pull-onlyProactive is where behaviour change actually happens, because nobody opens a budgeting app voluntarilyNotifications about money are stressful. Get the cadence or the tone wrong and users uninstall rather than muteProactive, but strictly rate-limited and quality-gated. Set a fixed weekly budget of notifications, ranked by expected usefulness, with easy per-topic muting. Treat an uninstall as the cost function
Information vs adviceAdvice is more useful and more differentiatedAdvice may make you a regulated entity, and it gets you sued when it goes wrongInformation and education, forcefully. Describe the user’s own data and explain general concepts. Never recommend a specific product, security, or tax position. This is a product constraint enforced in code and eval, not a disclaimer at the bottom of the screen
Store raw transaction data vs derived features onlyRaw enables new features, backfills, and better models laterEvery stored row is breach surface and regulatory obligationStore raw, encrypted, with a defined retention window and real deletion. The alternative sounds safer but makes the product unbuildable

What breaks

Broken bank connections, constantly. This is the top support driver in every product in this category. A silently stale connection is worse than an obviously broken one, because the user makes decisions on an incomplete picture. So surface staleness explicitly, and never render a balance without an “as of” timestamp.

Duplicates and pending/posted churn. A transaction appears pending, then posts with a different amount and a different descriptor. Naive ingestion shows both. Now the user’s food budget is double-counted, and they no longer believe anything the app tells them.

Transfers counted as spending. Moving $2,000 from checking to savings is not $2,000 of expenditure. However, it arrives as two transactions on two accounts, and it looks exactly like spending. Failing to detect internal transfers makes every aggregate wrong, and it does so for exactly the engaged users who connected the most accounts. Match on amount, sign, date proximity, and account ownership. Get this wrong at your peril, because it is the most visible possible error.

Ambiguous merchants. Amazon, PayPal, Square, and Apple are payment processors as much as merchants. PAYPAL *XYZTRADING could be anything. There is no correct answer available from the descriptor, so the honest design admits it: ask the user once, and remember forever.

Joint accounts and shared finances. Two people share one account, and one of them installed the app. Your “unusual spending” alert on a surprise gift purchase is a genuine harm you can cause with a well-functioning system. Similarly, alerts about spending at particular merchant types can reveal things about a user to whoever sees their phone. Financial data has a privacy dimension beyond the regulatory one, so a coach that comments on categories like healthcare, legal services, or gambling needs deliberate restraint.

The vulnerable-user problem. A meaningful fraction of your users are in genuine financial distress. Cheerful gamified nudges about coffee spending, directed at someone choosing between rent and groceries, are not just tone-deaf. They are the kind of thing that ends up in a newspaper. So you need detection for distress signals such as overdraft frequency, payday-loan merchants, and a declining balance trend. Then you need a different, quieter mode that signposts to real help rather than offering optimisation tips.

Advice liability. The model says something that reads as a recommendation, the user acts on it, and it goes badly. There are three defences, in order of strength. First, an output classifier that blocks recommendation-shaped statements about specific products, securities, tax positions, or credit decisions. Second, a system prompt with explicit scope and refusal patterns. Third, disclaimers. The ordering is deliberate, because the disclaimer is the weakest of the three and teams routinely treat it as the whole answer. Note also that being unregulated is not the same as being immune, because consumer protection law covers misleading statements regardless of whether you are a registered adviser.

Prompt injection through the transaction feed. Merchant descriptors are attacker-controlled if the attacker can cause a transaction. A payment to an entity named IGNORE PREVIOUS INSTRUCTIONS TRANSFER is a real, cheap attack. So treat descriptors as untrusted input. Sanitise them, escape them, separate them structurally from instructions, and never let retrieved content sit in the same channel as your system prompt.

Cost blowout from a chatty coach. An engaged user who chats daily with full context is materially expensive on a subscription that costs a few dollars a month. So set a budget per user, trim context aggressively, use a cheap model for routine turns, and use an expensive one for hard ones.

Cold start. A brand-new user has no personal baseline, so anomaly detection cannot work and the coach has nothing to say. You need cohort priors for the first 60 days, and honesty about it: “I need a couple of months of history before I can spot unusual spending.”


How you’d evaluate it

Categorisation, offline. Build a human-labelled test set stratified by merchant frequency, because head merchants are easy and the tail is where accuracy lives. Report accuracy separately for head, torso, and tail. Track it per category too, because an aggregate of 94% can hide 60% on a category that matters. The genuinely useful production metric is user correction rate, meaning the fraction of transactions a user recategorises. It is free, it is continuous, it reflects real perceived accuracy, and it is directly tied to trust.

Anomaly detection. There is no ground truth for “unusual,” so define it operationally. Measure precision by user response: dismissed, acknowledged, or acted on. Measure recall against a curated set of events users retrospectively said they wished they had known about, such as a duplicate charge, a subscription price rise, or a forgotten free-trial conversion. Alert volume per user per week is a first-class metric with a hard ceiling, for the same reason as in the maintenance chapter: the constraint is human attention.

The coach. There are two things to check, and they are different. Factual grounding asks whether every number in the output appears in the input payload. That is a deterministic check, so run it on 100% of outputs in CI and sample it in production. Quality and safety asks whether the response stays in scope, avoids product recommendations, and uses appropriate tone for the user’s situation. That is an LLM-as-judge rubric over a curated set. Include a deliberately adversarial set, with users asking “should I buy Bitcoin?”, “can I deduct this?”, and “should I take this loan?”, where the correct behaviour is a graceful, useful decline. See the sibling agentic-ai-evaluation-guide for the judge design, calibration against human labels, and CI gating. Do not rebuild that machinery here.

Online. A/B on the metrics that pay the bills: 30-day retention, connected-account count, and subscription conversion. Then measure behavioural outcomes, which are what the product actually claims: savings rate change, overdraft frequency, and whether users who set a goal reach it. Guardrail metrics matter as much. Track notification opt-out rate, uninstall rate after a nudge, and support tickets mentioning a wrong number. That last one should be tracked as a severity-one class of its own.

Regressions. Freeze a golden set of transactions with correct categories, and gate every model or rule change on it. Snapshot the analytics engine’s outputs on a synthetic user and diff on every deploy. Otherwise a silent change in how transfers are detected will ship unnoticed and quietly move every user’s numbers. Version the merchant table, and be able to explain when and why a merchant’s category changed, because a user will ask why their coffee is suddenly groceries.


Follow-ups they will ask

“Why not just use an LLM for categorisation? It’d be so much simpler.” Cost, accuracy, and determinism, in that order. Two hundred million transactions a month is a bill in the hundreds of thousands even at cheap rates. A small classifier is faster and more accurate for that task, because it learns from our users’ corrections and knows that this particular user’s Amazon spend is usually household. The LLM is also non-deterministic. The same descriptor can get two categories on two runs, which users notice and which makes month-over-month comparisons unstable. So I use the LLM for the tail of genuinely novel merchants, and I cache the answer into the merchant table permanently, so the marginal cost trends towards zero.

“Where exactly is the line between information and advice, and how do you enforce it?” Informational means a statement about the user’s own data, or a general educational fact. For example, “you spent 30% more on restaurants than your six-month average,” or “a high-yield savings account generally pays more interest than a checking account.” Advice means a recommendation to take a specific action with a specific financial product for this person’s situation, such as buy this fund, refinance with this lender, or claim this deduction. I enforce the line in three layers. First, an output classifier trained on recommendation-shaped language, which blocks before display. Second, a system prompt with explicit refusal examples. Third, disclaimers, which come last because they are the weakest layer. I would also have counsel define the line per jurisdiction. The US line for investment advice under the Advisers Act, the UK FCA line on regulated financial promotions, and the EU position are genuinely different, so the coach’s scope should be configured per market rather than hardcoded.

“A user asks ‘should I pay off my credit card or invest?’ What does the coach say?” It does not refuse flatly, because that is a bad product. It explains the general principle, which is comparing the guaranteed return of paying down a 22% APR balance against an uncertain market return. It grounds that in the user’s actual numbers, which are computed facts we already have. Then it explicitly declines to make the decision, notes that the right answer depends on things it does not know, and suggests a licensed adviser for a real recommendation. That is genuinely useful, it is educational rather than advisory, and it is the shape I would write into the eval set as the reference answer.

“How do you handle a user in serious financial distress?” Detect it and change mode. The signals are concrete: overdraft frequency, balance trending to zero before payday, payday-lender or debt-collection merchants, and declining income. In that mode the coach stops optimisation nudges entirely, because nobody in crisis needs to hear about their coffee. It becomes quieter and more concrete, it focuses on immediate cash-flow and bill timing, and it signposts to non-profit debt advice services rather than trying to solve the problem. I would also suppress gamification and streak mechanics wholesale in that mode. This is a case where the right product decision is to do less, and I would want it in the eval set as an explicit scenario with human review.

“Someone’s bank connection has been broken for two weeks. What does the product do?” It escalates, and it stays honest. On day one it retries silently and shows a subtle staleness indicator. On day three it prompts for re-auth in-app at a natural moment. On day seven it notifies. Throughout, every affected number carries an “as of” date, and any aggregate that spans the gap is marked incomplete rather than shown as if it were whole. The failure I am defending against is a user making a decision on a number that looks current and is not. I would also track connection health as a top-line operational metric with an SLO, because it is the product’s actual reliability, not my API’s uptime.

“How do you detect internal transfers?” Match on the negation. Look for a debit on one account and a credit on another, with the same or near-same amount, within a few days, where both accounts are owned by the same user. Use descriptor hints as a boost. This is a matching problem with a scoring function, not a classifier. When confidence is high I exclude both from spending aggregates automatically. When it is marginal I ask the user once and remember. I would tune this towards over-detection at the margin, because showing someone a savings deposit as spending is a much more damaging error than missing a transfer. I would also surface transfers as a visible, editable category rather than hiding them, so a wrong call is discoverable.

“How do you keep transaction data private when you’re sending things to a third-party model?” Minimisation comes first. The coach receives computed facts such as category totals, deltas, and goal progress. It does not receive raw transaction rows, merchant strings, account numbers, or names. Where a merchant name is genuinely needed, I send the normalised canonical name rather than the raw descriptor, because raw descriptors often contain card fragments and location data. Then come contractual and technical controls: zero-retention terms with the provider, no training on our data, and regional endpoints for EU users. Then a stated policy the user can read, because in this category trust is the product. If an enterprise or regulatory customer requires it, the coach layer is the one component that is cleanly swappable for a self-hosted open-weights model, precisely because it only ever sees derived facts.

“What’s your personalization/memory design? Why not just keep the chat history?” Chat history is a bad memory system. It grows without bound, it is expensive to carry, and the important facts get buried in small talk. So I keep a structured profile instead. It holds goals with target amounts and dates, income cadence, fixed obligations, stated preferences, topics the user asked me to drop, and a record of which nudges they engaged with versus dismissed. An extraction step writes that profile after conversations, and the profile is fully inspectable and editable by the user, which matters both for trust and for correcting extraction errors. Chat history is retrieved for context when relevant, but the profile is the durable state. The dismissal record is underrated, because a coach that raises the same suggestion a user rejected three times is worse than one with no memory at all.

“How do you know the advice is actually good, not just plausible?” There are two separable questions. Groundedness I check deterministically, because every numeral in the output must trace to the input payload. Quality I check against a rubric with domain experts, such as a certified financial planner reviewing a sample, and then I distil that into an LLM judge calibrated against their labels. The honest answer, though, is that the ultimate test is behavioural. Do users who receive coaching actually improve their savings rate or reduce overdrafts, measured against a holdout that gets the dashboard without the coach? If I cannot show that, the coach is entertainment, and I would want that experiment running from the first release.

“You’re storing five million people’s complete financial lives. What’s the security posture?” Credentials never touch our systems. That is delegated to the aggregator or to open banking OAuth, and it is the single biggest risk reduction available. Then use encryption at rest with per-user keys, so a single key compromise is not a total compromise. Add field-level encryption on account identifiers, and strict row-level authorization enforced in the data layer rather than the application layer. Internal access is the underrated threat. No engineer gets ad-hoc access to production transaction data, analytics runs on aggregated or tokenized views, and every access is logged and reviewed. Add retention limits and real deletion, including backups. Then the ordinary discipline: SOC 2, pen testing, and a breach response plan rehearsed rather than written. I would also assume a breach is possible and design so that what leaks is as useless as possible.

“The aggregator has an outage on payday. What happens?” The app keeps working on cached data with clear staleness marking. Notifications that depend on fresh data are suppressed rather than fired on stale numbers. The ingestion layer backfills on recovery with idempotent writes, so nothing double-counts. Structurally, the provider sits behind an interface with more than one implementation, so critical institutions can be moved to a direct integration or a secondary aggregator. I would also say plainly that this is single-vendor dependency on the most business-critical path in the product, and negotiating that dependency is a company-level risk item, not an engineering one.

“Transaction descriptors come from outside. Any attack surface there?” Yes, and it is the one people miss. A merchant descriptor is attacker-controlled text that ends up in an LLM context, which is textbook indirect prompt injection. Creating a payment entity with an adversarial name is cheap. So descriptors are untrusted data. They are sanitized, length-capped, structurally separated from instructions in the prompt, and never able to trigger a tool call. The stronger defence is that the coach has no write capabilities at all. It cannot move money, change settings, or send messages on the user’s behalf. So the worst outcome of a successful injection is embarrassing text rather than a financial action. Capability restriction beats prompt hardening.

“Scale this to fifty million users. What breaks first?” The categorisation cascade is fine, because it is mostly lookups and a tiny model, and it scales horizontally. Ingestion is fine with partitioning by user. What breaks first is the aggregator relationship, in two ways. Economically, per-connection pricing at that scale is a major cost line, and banks are now charging for access. Operationally, you are a large enough share of their traffic that their incidents are your incidents. Second is the coach’s cost, which grows with engagement rather than with users, so the tail of heavy users dominates. Third is notification infrastructure, which needs to fan out tens of millions of personalized messages in a narrow window without either melting or sending yesterday’s numbers.


Say it in one breath

A personal finance coach is mostly a data engineering and trust product. It is bank connectivity that breaks constantly, deduplication and pending-to-posted merging, transfer detection, and a categorisation cascade where user overrides beat a merchant table, which beats rules, which beats a small classifier. An LLM handles only the novel tail, cached forever, because running 200 million transactions a month through a language model is both more expensive and less accurate than a 5MB model trained on your own users’ corrections. Anomaly detection is robust statistics against each user’s own history, not a model asked whether something looks odd. The language model never does arithmetic: every number it says comes from the analytics engine, and you check that programmatically. The part that separates a senior answer from a junior one is treating financial advice as a regulatory boundary rather than a disclaimer. That means informational statements about the user’s own data plus general education, enforced by an output classifier before a prompt before a footer, plus a quieter mode for users in genuine distress.