Dinesh Sai ← All projects

Case study · iOSPrivate build, in daily use

Budget Tracker: money tracking that fills itself in.

A personal finance app built for how money works in India. Bank SMS alerts become transactions automatically, accounts, cards, loans and budgets stay in sync, and an on-device AI assistant answers questions about your spending without being able to make up a number.

  • Solo build: design and engineering
  • SwiftUI · SwiftData
  • Apple Foundation Models
  • Runs entirely on device
Services/AI/CardOffers.swift
// A model-extracted card offer is kept only if
// every figure in it appears in the source text.
static func isGrounded(_ offer: ExtractedOffer,
                       in source: String) -> Bool {
    let figures = title.matches(of: /\d+(?:\.\d+)?/)
    guard figures.allSatisfy(haystack.contains)
        else { return false }
    ...
}
Condensed from the repository. The same rule guards the monthly story and the chat.
16klines of Swift
101unit tests
7AI tools for the assistant
0servers, all on device

Counted from the repository in October 2026.

The problem

Expense apps fail at the first step: entering expenses.

Most budgeting apps assume you'll type in every purchase, or they connect to your bank through an aggregator, which means handing your financial data to a third party. In India, though, every UPI payment, card swipe and transfer already produces an SMS from the bank. The data is on the phone. It just isn't structured.

Budget Tracker turns those messages into a ledger automatically, on the device, and builds budgets, loans, card bills and insights on top, with no account, no server and no aggregator.

Rules I set

  • Financial data never leaves the phone.
  • The AI must never invent an amount, date, merchant or offer.
  • Card scans keep only the last four digits: never the full number, CVV or photo.
  • Logging an SMS must work in the background, within Siri's time limit.

What it does

  • Bank accounts, credit and debit cards, cash and wallets, with transfers between them.
  • Category budgets, savings goals, and loans with repayment schedules and reminders.
  • Credit cards with bill-date and due-date tracking.
  • Spending widgets on the home screen and lock screen.
  • Face ID app lock.

How it works

From a bank SMS to a categorised transaction, hands-free.

  1. Capture

    An iOS Shortcuts automation fires when a message notification arrives and passes the text to the app's Log Bank Message App Intent, which runs in the background without opening the app. It can also attach the current location.

  2. Parse

    A regex parser reads debit, credit, UPI and card messages: amount, account or card last four digits, merchant and reference number. For free-text entries, Apple's on-device Foundation Models extract a structured transaction. That call is capped at 6 seconds because Siri only allows an intent a few seconds, and it falls back to the regex parser.

  3. De-duplicate and match

    The same payment often triggers two messages. Duplicates are caught by reference number within a three-day window, or by amount and direction within three hours. A debit in one of your accounts and a matching credit in another are linked as a transfer, not counted as spending.

  4. Update everything

    A balance ledger keeps account balances consistent with transactions, and the widget data is refreshed on every save so the home-screen totals are always current.

On-device AI

An assistant that answers from your data, or not at all.

Everything runs on Apple's on-device Foundation Models: no API key, no cost per query, and nothing sent off the phone. The hard part was making a small model trustworthy with money.

tool calling

The chat assistant can't answer from memory. It must call one of seven tools (spending summary, transaction search, accounts, budgets and bills, subscriptions, card offers, goals) for every figure.

number guard

The monthly "money story" is checked so that every number in it appears in the computed facts, or the story is rejected.

grounded offers

Card offers are read from bank offer messages and pages, and each is kept only if its figures and merchant appear in the source. There's no offers database for the model to invent from.

receipts

Vision OCR plus the on-device model turn a photo of a receipt into a structured transaction.

insights

Subscription detection, unusual-spend alerts, a weekly digest, budget suggestions, a bills plan, and natural-language transaction search.

forecast

The home chart projects month-end spend from the 30-day everyday average plus scheduled EMIs, which are shown separately from everyday spending.

The pattern: the statistics are computed in plain Swift, and the model only phrases them. That split keeps the numbers exact while the words stay natural, the same idea behind the grounding checks in Dinapatra.

Engineering decisions

Trade-offs worth explaining.

Why SMS parsing instead of a bank aggregator?

Aggregators need credentials or consent flows and send your data to a third party. Bank SMS is already on the device, covers every bank, and keeps the whole app offline. The cost is parser maintenance, which the test suite exists to catch.

Why on-device models only?

Personal finance is the most sensitive data most people have. On-device inference means no API keys, no per-query cost, and nothing to leak. Because the model is small, the app doesn't trust it with arithmetic: Swift computes, and the model only writes the sentences.

How does the app survive a corrupted database?

The SwiftData store is shared through an App Group with the widget. If it can't be read, the app moves it aside and starts fresh instead of crashing on launch, which keeps the old store available for recovery.

Why did the Face ID lock loop, and how was it fixed?

iOS marks the app "active" again after the Face ID sheet, Control Center or a notification closes, so locking on every activation re-locked the app right after each unlock. The fix: the lock lives in its own window, and the app re-locks only after a real trip to the background longer than a chosen grace period. The lock also can't be turned on until one successful check, so you can't lock yourself out, and the behaviour is covered by tests.

Stack

Built with

Want a walkthrough?

It's a private build that I use every day, so the code isn't public. I can demo it or walk through the parsing and the AI guardrails.