Inspiration Every banking app I have warns me I am about to overdraft. Not one tells me what to do about it. The alert is correct and useless — it names the problem on the day the problem is already unavoidable.
I wanted the other half of that sentence. Not "you will go under on the 24th," but "skip this, cancel that, pay the minimum instead of the full statement, and you won't."
What it does Safe to Spend takes a transaction history and returns the smallest set of dated spending changes that keeps your daily balance above zero until payday — then proves nothing smaller works.
Every change is load-bearing. Drop any one and you go under again, by a stated amount. That is checked against a zero balance, not the cushion, and re-verified independently after the plan is chosen — so the solver is not marking its own homework. Dates, not just amounts. A change you cannot action in time is not a plan. When nothing clears, it says so in money and dates. It never calls your situation "infeasible" — it names the outside amount needed and the day it is needed by. Ask it why. An explainer in its own Ask tab answers questions about the plan, and can offer to change what the solver is asked. An offer arrives as a button you tap; it never applies itself. Money is integer cents end to end. No float touches it.
How we built it An exact CP-SAT model (Google OR-Tools) over a covering formulation — not a heuristic and not a greedy pass. Minimality is a proof obligation: when a solve is not proven optimal, the wording softens and the badge drops the word "proven". FastAPI on Python 3.14, stateless — no accounts, no database, nothing stored. Vite + React + TypeScript + Recharts. Deployed on Vultr behind Caddy, on a GoDaddy Registry domain.
mockSolver.ts is a separate brute-force implementation of the same problem. The Python suite shells out to Node and runs it as an oracle, so the fast solver and the slow obvious one have to agree. It doubles as the offline fallback: if the API is unreachable the browser solves locally, and the page says so.
Data comes from Capital One's Nessie sandbox, a bank CSV importer that infers recurring bills by cadence, or built-in demo accounts. Gemini runs the explainer on one rule: the solver computes, the model explains.
It was built by directing Claude Code and OpenAI Codex as parallel sessions, each in its own git worktree — seventeen by the end. The part worth stealing: Codex reviewed Claude Code's work adversarially and found real defects — a bypassable chat gate, a conversation that could lock itself, a minimality claim that was not a proof. One model implementing and a different one reviewing caught what neither catches marking its own homework.
Challenges we ran into Bugs that made accounts look safer than they were. The scariest defect here is not a crash — it is an account that renders as fine when it is not. One audit of the CSV importer found nine serious defects and six erred in that direction. That is why the minimality check was rewritten to re-verify independently instead of trusting the solver's own claim.
A credential check that had to write. Nessie answers a completely invalid API key with 200 OK and an empty list, not 401. A read can never distinguish "bad key" from "no data", so a misconfigured app fails silently and just looks empty. The check had to become write-then-read.
A model that retired mid-build. gemini-2.5-flash began returning 404 for new users during the event, and its replacement answered "currently experiencing high demand" on a live call. The explainer grew an ordered fallback chain — which I watched fire in production tonight.
Accomplishments that we're proud of We spent the weekend building a neural forecaster, proved it was worse than a one-line rule, and shipped the one-line rule.
A separate workstream trained a shared Transformer over pooled, license-audited transaction data — four sizes from 18.7k to 2.1M parameters, three seeds each, predicting fourteen days of spending from fifty-six observed days. We wrote the promotion gate before seeing any results: beat the baseline by 5%, and never underpredict worse than 5%, or it does not ship.
It never got through. The best version beat its own anchor by 19.5% on corpus validation and then lost on a real account, on both dev and test — the third time a corpus win failed to transfer. Everything that looked like an improvement was an improvement on data that was not the problem we were solving.
What shipped instead is the boring thing that measured better: spending assumed at a percentile of your own last eight weeks, same weekday. One line of arithmetic. The only thing the whole modelling effort actually contributed was choosing which percentile — and that turned out to be the entire win.
In a tool about somebody's rent, a model that is better on average but worse at the bottom of the distribution is the wrong trade.
Also: nearly 2,400 backend tests and over 350 frontend tests, including cross-implementation parity against the TypeScript oracle. Typical accounts solve in single-digit milliseconds.
What we learned Don't overengineer. Simple is often better — and you only find that out by measuring. The temptation was to keep tuning until the impressive thing won, and the gate is the only reason we didn't. "Simple is better" is easy to say and expensive to obey, because the simple thing is never the one you wanted to have built.
The other half is refusing to overstate. It says "sufficient under the schedule shown," never "guaranteed." It never says "infeasible" to someone short on rent. It discloses when a result came from the offline fallback rather than the real solver. Every one of those is a place where a more confident sentence was available and would have been wrong.
And a negative result you act on beats a positive one you cannot defend.
What's next for Safe to Spend The forecasting question is closed for this problem — three independent attempts all landed on the simple rule's curve, and we are not going to keep asking a question that has answered itself. A model might still earn its place somewhere we could not test this weekend: many real accounts, with bills detected as they arrive and pay cycles that are not fortnightly.
Nearer term: real account connectivity past a sandbox, and recurring-bill detection that survives merchants renaming themselves.
Built With
- caddy
- capital-one-nessie
- cp-sat
- fastapi
- godaddy-registry
- google-gemini
- lucide-react
- node.js
- numpy
- or-tools
- pydantic
- pytest
- python
- react
- recharts
- tailwindcss
- typescript
- uvicorn
- vite
- vultr
Log in or sign up for Devpost to join the conversation.