← Back to Work

Case Study

A₹tha: A Finance App Where the AI Isn't Allowed to Do Math

Solo-built in 4 days. Every rupee figure is computed in TypeScript. Groq writes the sentence that explains it. The rule never broke.

Solo Builder · Product, Design & EngineeringJun 13–16, 2026 · 4-day solo build
FintechAIPersonal FinanceOpen Prototype

Built with:Next.js 16 · React 19 · TypeScript · Supabase · Groq (Llama 3.3 70B) · Tailwind v4 · Playwright

Case Study5Causal factors scored per verdict
0
Causal factors scored per verdict
0
Read-only reference datasets (RLS-locked)
₹0.0L
10-yr opportunity cost surfaced on an ₹80K spend check

Outcome

Shipped solo: 3-step onboarding, a causal-attribution verdict engine, and a 5-action Decision Lab. Zero TypeScript errors. Zero Groq JSON parse failures across 27 eval cases.

The Constraint I Set Before Writing Any Code

Most personal-finance apps have the same failure mode. They hand the user's income and expenses to an LLM and ask it to reason about their financial health. The LLM does something reasonable. Sometimes it's right. Sometimes it invents a savings rate, rounds an opportunity cost, or confidently describes a shortfall that doesn't match the numbers sitting three lines above it in the same prompt. This isn't a hypothetical failure mode. It's the same mechanism that got an airline held liable in 2024 for a fare policy its chatbot invented on the spot.1

Once that happens, you don't have a finance product. You have a slot machine with better copy.

DECISION

The constraint I set before writing a single component: math first, AI last. Every rupee figure a user sees (goal probability, causal contribution percentage, opportunity cost of a purchase) comes from deterministic TypeScript running against real Indian market data. Groq only receives numbers that already exist and writes the sentence that explains them. It cannot change the numbers.

This cost me time I didn't have. Writing and testing the weighted scoring function for causal attribution took an evening I'd budgeted for onboarding UI. I kept the rule anyway, because the alternative (an LLM that occasionally invents a percentage in a product about someone's money) isn't a tradeoff. It's a disqualifier.

What the Product Is

A₹tha builds a Financial Digital Twin per user: a structured model of their income, expenses, assets, and goal, seeded through a three-step onboarding and persisted in Supabase. Against that twin, the product answers one question no generic finance app answers: which specific factor is blocking your goal, and by how much?

The causal-attribution engine is the core. causalAttribution() checks a user's twin against five thresholds: savings rate below 20%2, EMI burden above 30% of income3, lifestyle inflation outrunning income growth, an emergency fund under three months of expenses4, and equity holdings under three times monthly income. Each triggered factor gets a raw weighted score. Those scores normalise into percentages that sum to 100, sorted by contribution. If nothing triggers, it falls back to a single honest output: goal timeline too short.

INSIGHT

The percentages Groq sees in the analyze-twin prompt are the output of that function. Groq writes the verdict sentence and a confidence score around them. It cannot change them. "Lifestyle inflation drives 38% of your shortfall" is a computed number, not a generated one.

The Decision Lab runs five actions against the same twin:

Simulate projects net-worth paths for an MBA, a home purchase, or a job switch using sipFutureValue() and compoundGrowth() against the same market return and inflation tables as the main verdict. Groq writes the assumption note and key risks. It never touches the path numbers.

Spend-check answers "should I buy this?" with two numbers the LLM doesn't see until they're already computed: emi_ceiling_breach and a 10-year opportunity cost at Nifty 50's real CAGR.5 For an ₹80,000 phone bought today, that opportunity cost lands around ₹1.7 lakh. The route recomputes it after every Groq call to make sure the model hasn't rounded it into something friendlier.

Credit-card match has no Groq involvement at all. It scores the card catalogue against the twin's income, EMI load, and spend categories in pure TypeScript and returns the top three.

Chat is the one surface that runs in text mode: temp 0.4, capped at 400 tokens, grounded in the same twin data as everything else.

The Architecture Decision That Made This Work

Two structural choices did most of the work.

Reference data is read-only by design

Six tables (market returns, city inflation, property appreciation, term-insurance premiums, tax slabs, and a credit-card catalogue) are seeded once via a service-role script and locked read-only through RLS for every authenticated user. The app can query Nifty 50's 10-year CAGR from mfapi.in or RBI's city-level CPI. It can never mutate them.

That boundary is what makes the causal-attribution math trustworthy. The same twin, run twice, produces the same percentage. Nothing in the read path can drift.

Groq gets a context block, not a blank sheet

Every structured API route (analyze-twin, simulate, spend-check) passes Groq a block of pre-computed values and asks it to narrate, not calculate. The system prompt for spend-check explicitly tells Groq that emi_ceiling_breach and opportunity_cost_10yr are already computed and to pass them through unchanged.

The eval harness exists precisely to catch the cases where Groq ignores this instruction. Across 27 test cases, it didn't.

What I Scoped Out and Why

NOTE

A₹tha's onboarding is self-reported. The user types their income and expenses. That is the right call for a time-boxed solo build. It is the wrong call for a product where the accuracy of the twin determines the credibility of the verdict. Account Aggregator integration (real bank-statement data under explicit consent) is the obvious v2. I cut it because standing up a licensed AA partner integration in four days isn't a tradeoff. It's a fantasy.

Everything else I cut was an effort call, not a capability call.

SolutionReachImpactEffortScore
Deterministic causal-attribution engine (not LLM-estimated)top100%0.460.351.31
3-step onboarding + persona quick-fill100%0.300.251.20
Decision Lab (simulate, spend-check, credit-card, chat)80%0.400.550.58
Groq eval harness (27 cases across 3 personas)60%0.250.300.50
Playwright E2E + mocked API test suite50%0.200.350.29
Account Aggregator integration (real bank data)70%0.352.000.12

Internal scoring for what got built in the time box. Effort = engineering complexity (lower = harder). Score = reach × impact ÷ effort.

AA scored highest on potential impact and lowest on time-adjusted score. I parked it and moved on. The city list on Step 1 is hardcoded to 20 entries because querying property_appreciation directly adds a round trip I didn't need in onboarding. Both are one change away. Neither was the right thing to fix in the time I had.

Proving the AI Layer Behaves

A math-first architecture only holds if the narrative layer is actually reliable. "Trust me, I checked the prompts" isn't enough for a finance product. So I built scripts/groq-eval.ts before I called the build done.

It runs nine prompt variants (the verdict call, three simulate scenarios, two spend-check items, and three open-ended chat questions) against three fixed personas (graduate, mid-career, senior). 27 total cases. It checks that every JSON-mode response parses cleanly and every response returns inside a 5-second budget.

27
Eval Cases
9 prompt variants × 3 fixed personas
0
JSON Parse Failures
every structured response validated before caching
15,641
Total Tokens
one full eval run, llama-3.3-70b-versatile

The 0 parse failures number is the one I'd defend. It means Groq respected the output schema on every call. It does not mean the narrative content is always good. That's a different question, one that needs real users, not a harness.

Research Hypothesis

Hypothesis

A causal-attribution percentage, like 'EMI burden explains 34% of your shortfall,' will feel more actionable to a 22–32 year-old user than a generic health score or advice paragraph.

What we found

Untested with real users. No one outside the build has used the live product yet. This is the bet the entire architecture is built around, not a result I've measured.

Implication

The next real test is qualitative: put the dashboard in front of 10 people in the target age band and watch whether they read the causal bars before the probability number, or skip past them entirely.

How the Build Actually Went

  1. Day 1 · Jun 13Reference data before product screens

    Scaffolded Next.js and Supabase auth, then built the data layer: 16 market-return instruments from mfapi.in, a credit-card catalogue, city inflation from RBI CPI data, and property appreciation from NHB RESIDEX. The twin math can't run without real data underneath it, so I didn't touch a product screen until the reference tables were clean.

  2. Day 2 · Jun 14The whole product, under deadline

    Built onboarding steps 2–3, financial-math.ts and the causal-attribution engine, spend-check and credit-card-match routes, Decision Lab scenarios, the demo and temp-session system, the first test suite, and the eval harness, all in one day. That's what a clean Day 1 scope buys you.

  3. Day 3 · Jun 15Design system

    Moved source into src/, rewrote the type scale onto Hanken Grotesk + Instrument Serif, replaced placeholder styles with a warm ivory / sage-green token system. The product was working before it looked right, which is the correct order.

  4. Day 4 · Jun 16Hardening

    Fixed demo-twin propagation bugs, documented the architecture contracts in AGENTS.md, ran two passes of Vercel's React performance guidelines. Called it done.

What I'd Do Differently

I'd write the schema and API contracts on Day 1, not mid-build. I wrote the field-name contracts (which columns exist, what they're called, what the canonical aliases are) as I discovered I needed them. Early code and later code disagreed on naming until I froze the contract and back-filled the rule. A ten-minute decision on Day 1 would have removed a full sprint of churn.

The second gap is the demo account. The first person who opens the demo dashboard triggers a live Groq call instead of an instant cached read, because I didn't pre-seed the twin_analyses cache for the demo user. It's a one-line seed script change. I noticed it on Day 4 and ran out of time. That's the kind of thing that matters for an early demo and barely matters once a product is in production, a useful reminder of where to spend time in each context.

What I don't know yet is whether the core bet lands. Whether a 26-year-old in Bengaluru, looking at a dashboard that says "lifestyle inflation drives 38% of your shortfall," finds that more useful than "save more." That's the only question that matters, and it needs real people, not an eval harness. The next session is qualitative.