Garzoni
Live on web, iOS and AndroidAI-powered financial learning platform
Garzoni teaches personal finance the way a language app teaches vocabulary — ten-minute lessons, daily streaks, spaced repetition, and AI tools that let you rehearse a money decision before you make it. I designed, built, shipped and now run all of it: the API, the web app, the mobile app, the billing, the infrastructure and the content pipeline.
- Role
- Founder & sole engineer
- Duration
- 21 months, ongoing
- Surfaces
- Web, iOS, Android
- Scale
- ~250k lines · 900+ commits
- Users
- 150+ on web, iOS and Android
- Backend
- Django 5.2 · DRF · Celery
- Clients
- React 19 · Expo
What made it hard.
One engineer, three surfaces
Web, iOS and Android had to stay in lockstep with no team to divide them between. Anything duplicated across clients was going to drift, so the architecture had to make sharing the default rather than a discipline.
Real money, three payment rails
Subscriptions sell through Stripe on web, StoreKit on iOS and Play Billing on Android. A user who subscribes on their phone must be entitled on the web the moment they log in — from three systems that disagree about what a subscription even is.
LLM cost scales with users, not revenue
An AI tutor, voice tutoring, vision-based receipt parsing and a daily re-ranked learning path all bill per call. On a free tier, naive implementations lose money linearly with signups.
Store review is a hard gate
Apple and Google review every release. Financial-education content invites extra scrutiny, so the compliance position — education, not advice; no bank execution; no custody — had to be designed in, not bolted on.
One repo, one core.
Choices, and what they cost.
Every one of these bought something and gave something up. The trade-off is the interesting half.
A pnpm monorepo with a shared TypeScript core
Problem
Two clients on different runtimes (browser and React Native) needed identical domain types, API contracts, validation and design tokens.
Approach
One repository: a Django API, a React 19 web app, an Expo mobile app, and a TypeScript core package that both clients depend on. Types, API client, formatting and design tokens are defined once and consumed everywhere.
Trade-off
Builds and CI are heavier than three small repos, and a careless change in core can break both clients at once. In exchange, an API change surfaces as a type error in every consumer at compile time instead of as a bug report from a phone.
Function calling instead of free-form generation
Problem
A tutor that answers in prose is impossible to verify, easy to hallucinate with, and cannot drive the app's own UI.
Approach
The tutor is built on OpenAI function calling. The model chooses from a fixed set of tools — look up a lesson, run a budget calculation, open a simulator — and the app executes them. Retrieval runs over the curriculum itself with pgvector, so answers cite material that actually exists.
Trade-off
It constrains what the tutor can say, and every new capability means defining a new tool rather than editing a prompt. That constraint is the point: the model routes, the application computes, and any number it quotes came from real code.
Hash short-circuiting the daily re-rank
Problem
The learning path is re-ranked daily by an LLM. Most users' inputs do not change between days, so most of those calls would pay to produce a result identical to yesterday's.
Approach
The inputs that determine ranking are hashed. If the hash matches the last run, the previous ranking is reused and no model call is made.
Trade-off
Ranking can lag a change until the next cycle, and the hash has to cover every genuine input or results go stale. The cost curve flattens against inactive users, which is most of any free tier.
RevenueCat as the single entitlement authority
Problem
Stripe, App Store and Play each model subscriptions differently. Reconciling three webhook formats, three renewal semantics and three refund flows into one answer to 'what is this user entitled to?' is its own product.
Approach
All three rails feed RevenueCat, and the backend asks one question — what is this user entitled to — then enforces per-tier quotas and paywalls behind that single answer.
Trade-off
A third party sits on the revenue path and takes a cut. Worth it: cross-platform entitlement is a notorious source of silent, revenue-losing bugs, and it is not where a solo engineer should be spending their originality.
Health-gated deploys and automated backups
Problem
With no one else on call, a bad deploy at the wrong moment is an outage that lasts until I happen to look at my phone.
Approach
Deploys are gated on health checks, backups run automatically, Sentry reports errors, and CI enforces pinned dependencies, vulnerability auditing and secret scanning before anything ships.
Trade-off
Slower releases and occasional false-positive rollbacks. Preferable to discovering a migration failure from a user's review on the App Store.
The unglamorous half.
What turns a demo into something people can pay for and keep using.
Engagement
- Streaks, daily and weekly missions
- Leagues and leaderboards
- Spaced-repetition review
- XP and progression
Identity & safety
- JWT auth, Google and Apple sign-in
- Brute-force protection
- reCAPTCHA Enterprise
- Sentry error tracking
Money features
- Plaid open-banking budgeting
- Statement import
- Receipt analysis via GPT-4o vision
- Per-tier quotas and paywalls
Reach
- EN/RO localisation
- Shared design tokens across web and mobile
- Cloudflare edge caching
- 20+ backend test suites
What I would do differently.
- 01
The monorepo earned itself back within weeks, but I set it up after the second client already existed. Doing that first would have saved a painful migration of duplicated types.
- 02
I under-invested in the content pipeline early. Lessons started as code, and moving them to data later cost more than authoring them as data from the start would have.
- 03
Cost controls like hash short-circuiting were retrofitted once the bill made them obvious. On the next AI feature I would budget the per-call cost during design, alongside latency.