Case study — Trackster

From a crumpled receipt to a reconciled ledger.

Trackster is an expense tracker for freelancers and small businesses. Photograph a receipt on Android, let Claude read it, confirm the fields it wasn't sure about, and watch it land in QuickBooks. Trips, split bills, recurring expenses and eight currencies, all reconciled in one home currency.

Timeline3 weeksJul 19 → Aug 10, 2026
Surface83API routes, 23 web pages
Mobile252Dart files, 25 feature folders
CurrenciesEightConverted once, at write time
TeamOne132 commits, one author
01 — The problem

The receipt is the truth. It just lives in a shoebox.

Freelancers mix business and personal spending across cash, cards and wallets, in more than one currency, and then try to reconstruct it for tax season. Typing receipts in by hand is the step everyone skips, so the books are always wrong by exactly the amount nobody entered.

Vision models can read a receipt now. The hard part is everything around that call: trusting the fields it's unsure about, working with no signal, syncing to the accountant's system, and keeping totals stable when the exchange rate moves.

02 — Role & stack

Solo — web, mobile,
backend, billing

Two apps, one API, one data model, and the monetization stack on top — Stripe on web, Play Billing on Android, ads for the free tier. Currently on a testing VPS, not yet in the Play Store.

Android appFlutter + Riverpod — camera capture, review queue, offline queue, PIN and biometric lock, push, in-app purchase.
Web dashboardNext.js 16 — spending, budgets, income, reports, VAT, recurring, trips, contacts and billing.
Extraction serviceClaude vision behind a driver interface with a mock fallback; the app runs with no API key at all.
Admin consoleUser management, plan overrides, runtime config, feature flags with percentage rollout, audit log, impersonation.
Web

Next.js 16 · React 19 · Tailwind 4 · Recharts · Zod

Mobile

Flutter · Riverpod 3 · go_router · dio · secure storage

Data

PostgreSQL · Prisma 6 · 25 models · 22 migrations

AI & sync

Claude Haiku 4.5 vision · QuickBooks OAuth2 · FCM push

Money

Stripe · Google Play Billing · AdMob / AdSense free tier

03 — What I built

A camera, a queue,
and a ledger

Scan to expense

Photograph a receipt; merchant, date, total, tax, line items and a category come back with a confidence per field.

Review queue

Low-confidence fields land in a queue instead of silently in the books; duplicates warn but never block.

QuickBooks sync

Each expense is pushed as a Purchase with the receipt attached, through a database-backed retry outbox.

Trips & settlement

Shared trips with invite links, per-member shares and a debt simplifier that settles n people in n−1 payments.

Recurring bills

Daily to yearly templates that materialize into real expenses — with no cron job anywhere.

Business vs. personal

A scope on every expense, a VAT report, and CSV, Excel and PDF exports for the accountant.

04 — Deep dive

Trust the model, verify the field

The extraction call is the easy part. The product is what happens when the model is 72% sure about the category: that field goes to a review queue, the other four are accepted, and the user fixes one thing instead of retyping five.

  • Claude Haiku 4.5 by default, at roughly $0.004 per receipt; the model is a config value.
  • Every field carries a confidence score; anything under threshold routes to review.
  • A mock driver returns realistic data when no key is set, so the whole app runs offline in dev.
  • Free tier meters scans per month; Pro and Business lift the cap.
Free20 / moScan meter · ads
Pro$9 / moUnlimited · no ads
Business$29 / moQuickBooks · VAT · team
05 — Multi-currency

Totals that don't
move while you look

Eight currencies, one home currency, and a rule: convert exactly once. Every chart, budget bar and export reads the same denormalized column, so the dashboard and the CSV can never disagree with each other.

One expense, six steps
01CaptureAmount in the receipt's own currency, stored as entered.
02ConvertFX rate looked up once, at write time, from a per-day cache table.
03DenormalizeHome-currency total written beside the original — never recomputed at read.
04AggregateInsights, budgets and exports all sum the home-currency column, so they always agree.
05DegradeStale rate, then null, never a thrown error. A missing rate can't break the dashboard.
06RebaseChange your home currency and every row is recomputed, synchronously, once.

The same discipline runs through trips: shares and settlements are integer cents, and the simplifier guarantees the fewest transfers with no rounding residue.

06 — Engineering decisions

Fewer moving parts,
more guarantees

01
Recurring expenses without a cron jobScheduling

Due templates are materialized when the app calls /api/me — on every open. Missed periods are backfilled inside one transaction alongside the next-run advance, capped at twelve per template per call, and the function never throws.

Why — A cron on a personal VPS is one more thing to break silently. The user opening the app is the most reliable clock there is.

02
Idempotent by (user, request id)Offline

The mobile app queues expenses in secure storage and drains them on reconnect, resume or pull-to-refresh. The server enforces a unique constraint on user plus client request id — a follow-up migration scoped it per user after a cross-user collision.

Why — A retry that creates a second expense is worse than a failed sync. The database, not the client, has to guarantee once.

03
Convert at write, sum at readCurrency

Every expense and income row carries a denormalized home-currency amount. No aggregate ever calls the FX service; they all sum one column.

Why — Live conversion means a dashboard whose totals drift while you watch. One conversion per row means the numbers are stable and auditable.

04
Settle a trip in n−1 paymentsAlgorithms

A dependency-free greedy min-cash-flow pass over integer cents turns a trip's tangle of shares into the fewest transfers, and a pure split module handles equal, percentage and exact splits.

Why — Nobody wants to pay six friends back; they want to pay one. Integer cents mean the last transfer never comes out at 0.01.

05
Every vendor behind a factoryArchitecture

Extraction, storage and FX are interfaces with swappable drivers: Claude or mock, local or S3 or Cloudinary, live or cached. No vendor SDK is called from a route handler.

Why — Model prices change, storage moves, and a demo has to run without a single secret in the environment.

07 — Scale

Two apps and a full monetization stack in three weeks.

25 models · 8 enumsExpenses, income, budgets, recurring templates, trips, settlements, sync jobs, FX rates, plans, flags and audit.
83 API routesEvery one returning the same envelope, validated by Zod, scoped by auth before any read, soft-delete only.
7 verification scriptsRecurring, FX, offline sync, duplicates, quick-repeat, line-item search and data ownership — run against a live database.
14-phase roadmap55 items shipped, 7 open — tracked in the repo, with each phase's verification notes beside its plan.

Built so the books are right by default.