Phrasebook: how it's built
A chat tutor for Punjabi.
Every reply comes back as what you said, the answer in The script Punjabi is written in, in India — The real app shows its replies in it. with a transliteration and a translation, feedback and new words. A words list with flashcards, a progress tab with the running cost; one user today, every table keyed for many. The real app teaches Punjabi; the wiremocks below use Spanish, as the try-it copy does.
Architecture
One lesson turn
No scheduler: the streak is worked out when the state is read.
- Lesson page → Web frontend: page load, a static bundle (request, sync)
- Lesson page → Tutor service: state, onboarding and each turn, called directly (request, sync)
- Tutor service → Postgres: last 12 messages; counters and cost per call (request, sync)
- Tutor service → Claude: one call per turn, at most 1,000 output tokens (request, sync)
- Tutor service → Audio cache: audio lookup by text (request, sync)
- Tutor service → Google voice: on a cache miss only; characters counted (request, sync)
- Lesson page → Browser voice: fallback on a failed clip: 503 without a key, 502 on error (event, sync)
The audio path
Repeat plays cost nothing; only a miss that reaches Google is counted.
- Speaker press → Browser cache: the clip may already be here (request, sync)
- Browser cache → Audio route: a miss asks the server, text capped at 300 (request, sync)
- Audio route → Audio cache: a stored clip is reused (request, sync)
- Audio route → Google voice: a miss calls Google (request, sync)
- Google voice → Audio cache: the new clip is stored (response, sync)
- Audio cache → Count characters: then the characters are counted, in the same commit (request, sync)
- Audio route → Browser voice: 503 without a key, 502 on a Google error; browser voice (response, sync)
Wiremocks
A mock-up of a screen — Here, a static picture of a screen, drawn to show layout and flow, not the working app. — a static picture, not the real app
The progress tab
A made-up progress tab, voice priced at the app's own rate: the total is Claude plus voice, and the average per reply counts Claude only.
Usage and cost
All made-up numbers, in the real tab's layout.
- Claude$0.0480Haiku, 28,800 tokens, 12 replies
- Google voice$0.03842,400 characters, $16 per One million — Shorthand in prices and limits, as in $1 per 1M tokens.
- Avg / reply$0.0040
- Total tokens28,800
- Input tokens24,000
- Output tokens4,800
The average per reply counts Claude only, as the real tab does.
Tutor model
- Claude HaikuSelected$1 in / $5 out per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokens, fastest and cheapest
- Claude SonnetSwitch anytime$3 in / $15 out per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokens, most nuanced, pricier
A mock-up of a screen — Here, a static picture of a screen, drawn to show layout and flow, not the working app. — a static picture, not the real app
A flashcard
A card from the Spanish script; the real app teaches Punjabi.
Words, flashcard view
Card 6 of 16
Both faces are shown on this static card. The real app teaches Punjabi; this one is from the Spanish script.
Running cost
| Resource | Figure | Status | Source |
|---|---|---|---|
| Claude Haiku input (the version the app pins) | $1 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokens | Published price | pricing table in the crud module, line 11 (dated 2026-06-15 by blame; line 7 says "Keep in sync with platform pricing") Read on 2026-10-17 |
| Claude Haiku output (the version the app pins) | $5 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokens | Published price | pricing table in the crud module, line 11 Read on 2026-10-17 |
| Claude Sonnet input (the version the app pins) | $3 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokens | Published price | pricing table in the crud module, line 9 Read on 2026-10-17 |
| Claude Sonnet output (the version the app pins) | $15 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokens | Published price | pricing table in the crud module, line 9 Read on 2026-10-17 |
| Output cap per call | 1,000 tokens per call | Set in the project's code | tutor module, line 73 Read on 2026-10-17 |
| Output-token cost ceiling per call, Haiku | $0.005 per call | Derived | derived (see inputs) Inputs: 1,000 tokens (configured) × $5 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokens (published); output only, the input size is unknown Read on 2026-10-17 |
| Output-token cost ceiling per call, Sonnet | $0.015 per call | Derived | derived (see inputs) Inputs: 1,000 tokens (configured) × $15 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokens (published); output only, the input size is unknown Read on 2026-10-17 |
| Sonnet over Haiku, price per token | 3× per token | Derived | derived (see inputs) Inputs: 3 ÷ 1 and 15 ÷ 5 (published); the app's own label, page line 25, says ~3× Read on 2026-10-17 Conflicts with readme, line 29 (~3–4×, read on 2026-10-17): That figure is about the bill, not the per-token price, and tokens per turn are unmeasured; the rates in the crud module govern the price. |
| Google voice | $4 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. characters | Published price | Google's text-to-speech pricing page (WaveNet, a legacy voice tier), read 2026-10-17; the app's voice tier is set in the config module, line 13 Read on 2026-10-17 Conflicts with pricing table in the crud module, line 21 (the comment at line 15 says 2025 pricing) ($16 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. characters, read on 2026-10-17): The vendor's page is newer and governs; the app's own screen still computes at $16, so its figures run higher than the page's. |
| Google voice free usage | 4 million characters per month | Published price | Google's text-to-speech pricing page (WaveNet, a legacy voice tier; free characters allowed per month), read 2026-10-17 Read on 2026-10-17 |
| Audio cap per clip | 300 characters per clip | Set in the project's code | tutor router, line 36 Read on 2026-10-17 |
| Audio ceiling per clip | $0.0012 per clip | Derived | derived (see inputs) Inputs: 300 characters (configured) × $4 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. characters (published); the app's own screen would show $0.0048 at its $16 rate Read on 2026-10-17 |
| Postgres | — | Unknown | owner's metered-resource table (no row names this project) Read on 2026-10-17 |
| Hosting | — | Unknown | owner's metered-resource table (no row names this project; the frontend's share of the Vercel edge-request plan is not recorded; the Railway row is marked verify and names another project) Read on 2026-10-17 |
The app's own screen adds Claude cost, kept per call in millionths of a dollar, to a voice cost worked out from characters.
Design decisions
- The key and the state live on the serverThe Claude key stays on the host, and the database is the truth. The browser keeps only display settings. The readme routes the browser through the frontend; the code calls the service directly, and the code governs.
- One user today, a seam for manyEvery table is keyed by user, so going multi-user means replacing one function. The cost: there is no auth yet.
- The model's JavaScript Object Notation — A lightweight text format for exchanging structured data. is not trustedFences are stripped; if parsing fails, the raw text shows. Live use produced two failures: The script Punjabi is written in, in India — The real app shows its replies in it. in the roman field, which hides the you-said panel, and an empty you-said field on short messages.
- Cost is added per call, in millionthsEach call is priced at that call's own rate, so "the tracker stays correct when models are mixed".
- Audio is cached and metered only on missesOnly a miss counts characters; without a key the server answers 503 and the browser voice plays.
Open questions
- Postgres: whether this project sits in the owner's Launch-billed Neon org, and how big the stored clips get
- Hosting: no owner row names this project; whether the frontend shares the Vercel edge-request plan is not recorded, and the Railway row is marked verify and names another project
- Tokens per turn: no measured figure in the repo
- Monthly spend: not recorded
- Google voice: whether the production key is set, and whether usage stays inside the free characters
- Model: the client always sends the learner's pick (Claude Haiku by default); which pick production traffic uses is not recorded