Back to Blog

Building an App That Refuses to Guess

I built a financial planning tool for my own situation and then started turning it into a product. The rule that shaped it: when a required fact is missing, say so, rather than producing a confident number.

Personal FinanceDeterminismArchitectureTypeScript

I built a financial planning app. It started as a tool for my own situation, which is a slightly awkward one involving self-employment income, and it has grown enough that I am now building it into something other people can use. I am keeping the name to myself until it is closer to ready.

This post is about the one decision that turned out to shape everything else.

The gap I was trying to fill

Money apps are extremely good at telling you where your money went. Almost none of them will tell you what to do next.

The questions I actually had were all forward-looking. How much cash is genuinely free after everything already spoken for? What do I need to set aside monthly to hit this by then? If I put this much toward debt, when is it gone, and what if I want it gone by a date instead? What is my next tax payment going to be, and am I setting aside enough?

Those are not reporting questions. They are planning questions, and they need a model of the future rather than a categorization of the past.

So the app has a ledger, virtual savings buckets layered over real balances so you can see what is genuinely uncommitted, goals with derived monthly contributions, a debt engine that solves for either a payoff date or a required budget, deterministic forecasting you can click into, scenarios you can compare without touching your real data, and self-employment tax estimation with quarterly targets.

The rule

The hard part was not where I thought it would be.

Tax estimation needs facts. Some of them the user has, some of them they do not, and some of them they think they have and are wrong about. The obvious engineering move when a fact is missing is to pick a reasonable default and carry on, because an app that returns a number feels finished and an app that returns a shrug feels broken.

That instinct is wrong here, and I think it is wrong in most places where the output is a number somebody will act on.

A confidently wrong tax estimate is worse than no estimate. Not slightly worse. Categorically worse, because the person stops thinking about it. They saw a number, the number looked authoritative, they planned around it, and they find out in April.

So the engine is built around bounded results. If a required fact is missing, it says which fact and what it would do to the answer, and it returns a range rather than a point. It does not invent the gap and it does not silently substitute an average. Every number carries its inputs, its assumptions, and the version of the rules it was computed under, so you can always ask why.

That is a worse demo and a better product.

No model in the math

The related decision, and the one people find surprising given how much of my other work is agent-heavy: no language model touches a financial calculation.

I use coding agents constantly. I have written about how I use them. And they have no business anywhere near the arithmetic here, for three reasons.

Reproducibility. The same inputs must produce the same output today and in six months, on my machine and on yours. Sampling breaks that by construction.

Auditability. When a number looks wrong, I need to trace it to a rule and a source. "The model said so" is not a trace.

Failure mode. When deterministic code lacks a fact, it can be made to stop. A model's failure mode is fluent plausibility, which is exactly the shape of failure this domain punishes hardest.

Where generation genuinely helps is around the edges: explaining a result in plain language, drafting a note, categorizing a messy transaction description as a suggestion a human confirms. All of that is fine, because none of it is load bearing. The rule is not "no AI." It is that no number a person will act on comes out of a sampler.

Reading, not moving

One more constraint that paid for itself many times over: the app reads and plans, and never moves money. No transfers, no bill pay, no trading.

That single line removes an entire category of catastrophic bug and an entire category of regulatory burden, and it costs almost nothing, because moving the money was never the hard part. Knowing what to do was. Once you know, moving it is a two-minute job in an app you already have.

I recommend this constraint to anyone building in this space. The temptation to add the button is strong and the value it adds is small next to what it costs you.

Where it goes

What I have is a single-household app that is very good at one person's situation. Turning it into a product means multi-tenancy done properly, onboarding for someone who did not build it, wider institution coverage for syncing, and tax rules that generalize past the one case I built against.

That last one is the real work. The engine is already structured around versioned, source-backed rules, but every jurisdiction added is a new set of primary sources to verify and keep verified as they change. That is not a feature you ship once. It is an ongoing obligation, and taking it seriously is most of what separates a tool that helps from a tool that quietly hurts people.

Which brings it back to the same rule. The app is allowed to not know something. It is not allowed to pretend.