Inspiration
Bank apps are good at the past: what you spent and where. The questions people actually have are about the future: will I be short next month, should I pay this card off or keep the cash, can I afford this car? Those answers need real arithmetic on the customer's own history, and they need to be explained in plain words. We didn't want a chatbot that might invent a balance or an interest rate. We wanted an assistant that only says what the numbers support, and shows you that it checked.
What it does
Guidestone is a bank dashboard for one signed-in customer (synthetic data, 100 customers).
- Accounts: balances, spending, recurring bills, and a 24-month cash forecast with an 80% uncertainty band. An advisor card gives a verdict (On track / Plan ahead / Act now) and up to three next moves.
- Pay Off: compares five payoff strategies on months, interest and what each does to the cash cushion. For one persona, minimum payments take 342 months; a safe lump sum plus highest-rate-first is debt-free in 6 months and saves $28,594 in interest, and the app says plainly that cash would dip under the cushion for 6 months in a bad stretch. For a customer whose mortgage costs 2.8% while savings earn 4.1%, it says keep your cash.
- Buy (cars): pick a segment, budget and priorities; get up to three cars from a cited research pack (every price, MPG and safety rating links to its source), then cash / finance / mix plans with a verdict (affordable / stretch / not now).
- Agent activity window: a live view of each run: planning, calculations, rules, writing, checking, retries, fallbacks.
There is no chat box. Opening a screen or pressing a button fires an event, and the agents run for that event.
How we built it
- Data: a deterministic synthetic bank generator (24 months of transactions per customer, an exact ledger), typed Parquet, and a
validating loader. Side generators add bio fields, estimated home and vehicle values, and demo logins on
example.com. - Forecast: a deterministic schedule of paychecks, bills and loans on real business days, plus a 1,000-path Monte Carlo of the customer's own spending (block bootstrap, seasonality, parameter uncertainty, simulated card balances, learned sweeps). Bands are calibrated with split-conformal scaling per horizon. A 24-month forecast takes about 18 ms.
- Decision rules as code: a priority waterfall, payoff priority and 14 candidate moves, every threshold in
assumptions.jsonwith a source or rationale. Verdicts are computed, never generated. - Agents: an orchestrator with an event registry (
dashboard_open,simulate_payoff,simulate_buy); a planner, an advisor and a researcher, all on Nemotron 3 Nano 4B served by llama.cpp'sllama-serveron the same machine, reasoning off. The model writes{placeholders}, the app fills them, and a grounding check verifies every figure; one retry, then a deterministic template. - API and UI: FastAPI with session-bound
/api/me/...endpoints and a server-sent-events stream; Next.js 16 + React 19 + Tailwind 4 + Recharts, with TypeScript types generated from the OpenAPI schema. - Tests: 426 passing pytest tests with a mock LLM transport, plus check scripts for the data, forecast, payoff and agents.
Challenges we ran into
- The forecast bands lied at first. The raw 80% band held the outcome only 60% of the time at month-end and 43% of days. Measuring one variance source at a time showed the real cause: customers without a card had very smooth spending (2.9% month-to-month variation against 25.6% for card holders), so their bands were about 3x too narrow. We added simulated card balances, parameter and seasonal uncertainty, noisy bills and learned sweeps, then calibrated per horizon on held-out customers: 82% month-end, 84% daily. Calibration then made bands narrow between day 30 and 90, so we added a rule that a band never gets narrower. One idea, utility seasonality, made the error worse (6.2% → 7.1%), so we left it off.
- A 4B model invents numbers. Asked to explain a forecast, it would write plausible dollar amounts that weren't in the data. We
stopped letting it type numbers: it writes
{placeholders}, the app fills them, and a checker extracts every figure from the final text and matches it to a fact, including the words around it ("overdraft", "a month", "lowest"). Judgment words and style are checked too. One retry, then a template built only from the facts. - Reasoning mode was too slow for a dashboard. With reasoning on the advisor took about 15 s (p90 25 s); with it off, about 4 s (p90 6 s)
at the same first-try pass rate. We turned it off and made up for it with structure: response schemas whose
enums list the only legal move codes or car ids, so the model can't pick something that doesn't exist. Along the way:tool_choice: "required"hung llama-server's grammar mode, and the planner answered{"steps":[]}to everything until each tool carried a one-line hint on when to use it. - Counting the same dollars twice. Our surplus already excluded money leaving for retirement accounts, and the waterfall then offered those same dollars to the cushion or the debt. We split out "free surplus" so existing retirement transfers count as progress, not as spare cash. In the same spirit: a card payment sliding from a Saturday into the next month made a month look like it had two payments, and month-end balances hid the dip just before payday (one plan's lowest month-end p10 was $8,077; its lowest daily p10 was $5,771), so cushion checks now use each month's daily low.
- Sources blocked our fetches. toyota.com and Toyota's pressroom PDF returned 403, so we couldn't cite a RAV4 price. Instead of filling
it from memory, the pack records
nullwith the reason, and a car without a cited price is never offered. 6 of the 24 vehicle figures are null for reasons like this.
Accomplishments that we're proud of
- A forecast with an honest, held-out accuracy number: the 80% band held the real outcome 82% of the time on customers it had never seen, and the typical six-month error is 6.2% of a month's spending. We document what it doesn't prove (all held-out customers share the same test months).
- Every figure the model shows is checked against the calculations, and the UI can say so.
- The app tells people when not to pay debt down, and states the tradeoff when it does recommend it.
- Privacy by construction: the research agent receives only a segment, a budget rounded down to $5,000, and priorities. Traces and streams carry no prompts, model text or customer data, and tests enforce it.
- Everything runs on one laptop, model included.
What we learned
- A small model is a good planner, ranker and writer once you take arithmetic and verdicts away from it. Structure (enums, placeholders, checks, a fallback) did more for quality than reasoning mode did.
- Measure one change at a time. Our first guess about the forecast's coverage problem (spending spread evenly across the month) was wrong.
- Pick the error metric that doesn't flatter you: MAPE was 1.8% at 180 days, but it divides by balances near zero, so we report error as a share of monthly spending instead.
- A null with a reason beats a confident guess, for sources and for the model.
What's next for Guidestone
- Plan: retirement and education planning on the same waterfall.
- Buy for homes, and live research instead of a cached pack.
- A Presenter agent for the summaries.
- A hosted fallback with a data minimizer, so a bigger model can help without receiving customer data (designed, not built).
- Forecast: within-month transaction timing (to shrink the daily band's large correction), an out-of-time backtest, and the remaining what-if types.
- Real data, with consent, to see which of our findings hold beyond the synthetic generator.
Built With
- apache
- arrow
- fastapi
- llama.cpp
- nemotron
- pydantic
- python
- react
- typescript
- uvicorn