Skip to content

Instantly share code, notes, and snippets.

@tankchintan
Created July 12, 2026 23:40
Show Gist options
  • Select an option

  • Save tankchintan/2ed8ed0d6fb2e91debf8dae892e1256a to your computer and use it in GitHub Desktop.

Select an option

Save tankchintan/2ed8ed0d6fb2e91debf8dae892e1256a to your computer and use it in GitHub Desktop.
Build Your Own AI Kids' Quiz App — A Hand-Off Brief

Build Your Own AI Kids' Quiz App — A Hand-Off Brief

A complete brief for building a personalized, AI-powered quiz app for your own kids. Written so you can hand it to a coding agent (Claude Code, Codex, Cursor, whatever) and have it build the whole thing — or read it top-to-bottom yourself and understand every decision. It's based on a real app running in production for two kids (ages 5 and 10); the hard-won lessons here are the ones you can't get from a fresh spec.


How to use this document

Two ways:

  1. Hand it to your agent. Paste this whole file into Claude Code / Codex and say "Build this for my kids. Ask me for their names, ages, grade levels, and interests first, then go." The spec is detailed enough to execute against. It'll fill in your kids' specifics where this brief uses two placeholder kids — a "son" (10) and a "daughter" (5) — as examples.

This is a template, not a fixed build — make it yours. The two example kids are just that: examples. Tell your agent the real names (or keep them anonymous — your call), and how many kids you actually have. One kid, three kids, five kids — it's all the same engine, one config object per child. Their profiles will differ from the examples too: a 7th grader, a 4-year-old, a teenager brushing up for a test — swap in the real ages, grades, reading levels, and interests. The engine and the lessons don't change; only the per-kid config does. 2. Read it, then cherry-pick. The architecture and the lessons (§6) are the valuable part. The exact stack (React, Cloudflare) is swappable — the design decisions are what transfer.

The single most important thing in here is §3, the pivot: we started with "every question is an AI call" and ended up deleting the AI from most of the app. If you skip everything else, read that. It'll save you the two days it cost us to learn it.


1. What you're building

A no-login, mobile-first web quiz where each kid taps a link and gets an endless stream of age-appropriate multiple-choice questions, with a per-kid save slot, difficulty levels, and a reward mini-game every few correct answers.

  • One shared game engine, parameterized by a per-kid config object. You do NOT fork the code per child. One route per kid (/quiz/<kid1>, /quiz/<kid2>, ...) passes a different config into the same component. Works for one kid or five — adding a kid is a config file, not a rewrite.
  • No accounts, no install. Kid opens a URL on a phone/tablet and plays. Progress auto-saves to localStorage.
  • The API key never touches the browser. A tiny serverless proxy holds the key and builds prompts server-side. The frontend is a thin client that sends parameters, not prompts.
  • Free infrastructure. Static site host + a serverless function free tier covers a household's usage with room to spare.

Our reference stack (swap freely)

  • Frontend: Create React App (react-scripts 5), React, TypeScript, Tailwind, react-router v6.
  • Proxy: a single Cloudflare Worker (worker.js) + Cloudflare KV for question history.
  • Model: Claude Haiku (fast + cheap; more than enough for grade-school questions).
  • Hosting: static site on any static host with git auto-deploy; the Worker deploys separately.

None of this is load-bearing. Next.js + a Vercel function + OpenAI would be a fine substitute. The shape is what matters: thin client → key-holding proxy → model, with history dedup in the proxy.


2. Architecture

  ┌──────────────┐   POST {player, typeIndex,    ┌───────────────┐   builds prompt,   ┌─────────┐
  │  React app   │   difficulty, seed}           │  Proxy Worker │   calls model      │  Claude │
  │ (thin client)│ ────────────────────────────▶ │  (holds key)  │ ─────────────────▶ │  Haiku  │
  │              │ ◀──────────────────────────── │               │ ◀───────────────── │         │
  └──────────────┘   question JSON               └───────┬───────┘   question JSON     └─────────┘
        │                                                │
        │ localStorage (L0):                             │ KV (question history, per kid):
        │ score, streak, difficulty, etc.                │ last 60 questions, for dedup
        ▼                                                ▼
   survives refresh                          cross-device "don't repeat" memory

Why the proxy builds the prompt (not the frontend): it keeps the API key out of the browser bundle and keeps all the prompt logic — personas, per-type instructions, difficulty text, the "don't repeat these" history — in one server-side place. The frontend just sends four numbers: which kid, which question type, which difficulty, a variety seed.

Question-history dedup lives in the proxy, not the browser. After each successful question, the proxy appends the question text to a per-kid history list (rolling window of ~60) and injects that list into the next prompt as "do not repeat any of these." Store it server-side (we used a key-value store) so a kid who plays on a tablet in the morning and a phone at night still doesn't see repeats. Use a two-level cache — an in-memory copy per server instance (L1) backed by the persistent store (L2) — and write to the store after returning the response (fire-and-forget) so the store write never sits on the critical path. This detail only matters for the AI-backed question types — see §3, most types won't hit the model at all.

The config-object pattern (the core of the whole thing):

interface PlayerConfig {
  playerName: string;         // the kid's name, e.g. "Son"
  localStorageKey: string;    // "son-quiz-state"  — separate save slot per kid
  pageTitle: string;          // "Son's Quiz 🎓"
  questionTypes: string[];    // the rotation of question categories for this kid
  typeInstructions: Record<string, string>;  // per-type prompt guidance
  difficultyConfig: Record<number, { label: string; desc?: string }>;  // 1 or 3 tiers
  personaPrompt: string;      // who the AI is talking to, tone, interests
  specialRules: string;       // hard constraints for this age group
}

Two routes, same engine:

<Route path="/quiz/son"      element={<QuizGame config={SON_CONFIG} />} />
<Route path="/quiz/daughter" element={<QuizGame config={DAUGHTER_CONFIG} />} />

The engine reads the config for: which question types to rotate through, how to scale the UI, which difficulty tiers to expose, streak thresholds, and whether to show text-to-speech buttons. Do not put a kid's name in a public URL — use /quiz/<name> under a section, not /<name> at the root, and don't name the proxy after one child (kids-quiz-proxy, not <name>-quiz-proxy).

The question JSON contract

Every question — whether AI-generated or code-generated (§3) — returns the same shape. Design this first; everything else conforms to it:

{
  "question": "What is 7 × 8?",
  "emoji": "✖️",
  "type": "math_multiplication",
  "answers": ["54", "56", "48", "63"],
  "correct": 1,
  "praise": "Yes! 7 × 8 = 56 — like 7 boxes of 8 crayons! 🖍️",
  "hint": "Think of 7 groups of 8.",
  "wrongContext": [
    "54 is 6 × 9 — close, but different numbers!",
    "",
    "48 is 6 × 8 — right 8, but you used 6 instead of 7.",
    "63 is 7 × 9 — right 7, but you used 9 instead of 8."
  ]
}
  • Exactly 4 answers, all distinct; exactly one correct index in [0,3].
  • wrongContext is length 4: "" at the correct index, a one-to-two-sentence why-this-is-a-common- mistake explanation at each wrong index. This is the pedagogical heart of the app — kids learn more from understanding why their wrong guess was tempting than from the right answer alone.

3. THE PIVOT — read this even if you read nothing else

We built this exactly as the plan said: a smart proxy, a two-level cache, "every question is one model call." The plan even had a "do not simplify the cache" warning. Within 48 hours of shipping it, the most important design move was to delete the AI from most of the app.

Why: the 5-year-old got 🦋🦋🦋🦋 How many butterflies? with answer choices [2, 3, 5, 1]. The correct answer, 4, wasn't even an option — the model miscounted the emojis it had itself chosen. Then: What color is 🌙? → the "correct" answer was "white," but the moon renders yellow/gold on every phone. The whole family picked yellow and got marked wrong.

The lesson: an LLM is the wrong tool for deterministic domains. Counting, arithmetic, "which shape has 3 sides," color identification — these have exactly one right answer computable in code, and the LLM adds latency, cost, and a nonzero hallucination rate for zero benefit. So we split the question types into two pipelines:

Pipeline Use for How it works
Deterministic (code) Anything with one computable answer: counting, arithmetic, shapes, colors, patterns, letters, opposites, rhyming, multiplication, division, measurement/time Seeded RNG + hand-authored content banks generate the question, the correct answer, and plausible distractors — in code, on the server, zero model calls, zero cost, <1ms, 100% correct
AI-backed (LLM) Open-ended stuff a human writes better: reading comprehension, vocabulary, science facts, geography, mythology, grammar, trivia Model call with persona + type instructions + difficulty + "don't repeat" history

Concretely, for our two kids: the 5-year-old's entire quiz became deterministic — zero model calls. The 10-year-old's 5 math types became deterministic; his 7 open-ended types stayed on the model.

Design implication for your agent: build the deterministic generators as pure functions of (difficulty, seed) returning the same JSON contract as the AI path. The proxy dispatches: if the requested type is deterministic, generate in code and return immediately; otherwise call the model. The frontend can't tell the difference — same contract either way.

How to write a deterministic generator (the pattern that matters):

  1. A seededRandom(seed) PRNG so output is reproducible and testable.
  2. A content bank for the domain (e.g. an emoji→color map, 100 word-problem scenarios, a letter→word→emoji bank). Go big — hundreds of entries — so questions don't feel repetitive.
  3. Compute the correct answer, then a generateDistractors(correct, ...) helper that produces 3 plausible-but-wrong options (e.g. off-by-one counts, adjacent multiplication facts), shuffled with the true answer.
  4. Invariant: the true answer is always one of the four choices. This is the butterfly bug, encoded as a rule that can't be violated. Add a test that fails loudly if it ever is.

4. Per-kid customization (the part that makes it theirs)

Same engine, deliberately different everything-else per kid. This table is the template — fill in your own children. Ours were a 5-year-old (pre-reader, starting Kindergarten) and a 10-year-old (Grade 4):

Dimension Younger kid (5) Older kid (10)
Question types letter-recognition, counting, colors, shapes, animals, opposites, rhyming, beginning-sound, simple-math, patterns multiplication, word problems, animal science, vocab, patterns, space, division, geography, mythology, measurement/time, grammar, trivia
Difficulty tiers 1 (hide the selector when there's only one) 3 (Normal / Harder / Challenge)
Reading load Emoji-first — she can't read fluently. Answers are single words, numbers, or single emojis. Never a full-sentence answer. Normal text; richer distractors and explanations
Content rules Positive phrasing only (never "which is NOT"); no clocks/money; no academic verbs ("calculate", "identify"); one idea per question No fractions; clean whole-number division (÷2–9, <100); 1–2 step word problems; culturally relevant facts
Persona Playful, celebration dialed up, framed as "already ahead" so the AI isn't condescending Warm host leaning into his interests (LEGO/space/mythology); "wow facts"
Praise style Specific, not generic. "Yes! 🐄 Cows say MOO!" — never "Good job!" Explains why it's correct in one enthusiastic sentence
UI scaling (isKid flag) Bigger fonts (clamp(1.4rem,5vw,1.8rem)), 60px tap targets, dark-contrast pill behind emoji clusters, category emoji removed (confusing) Standard fonts, 52px tap targets, category emoji kept
Text-to-speech 🔊 speaker buttons on the question and each answer (she can't read) None
Streak reward threshold Every 3 correct (shorter attention span → faster reinforcement) Every 5 correct (sustain focus, delayed gratification)

The personaPrompt + specialRules fields carry the tone and hard constraints into every AI prompt. Example of the younger kid's specialRules (verbatim from ours — it works):

- Questions must be answerable by a child who cannot yet read fluently. Rely on emojis, not sentences.
- Answer choices must be short: single words, single numbers, or single emojis. Never a full sentence.
- Never ask multi-step questions. One idea per question only.
- Never use academic instruction language ("calculate", "identify", "determine").
- Never ask about time (clocks, calendars) or money.
- Never use negative phrasing ("Which one is NOT a...") — always ask positively.
- Praise must be warm and specific.

5. Engagement layer (optional but it's what makes them come back)

  • Streak charging bar. A bar that fills as the kid answers correctly in a row; at the threshold (3 or 5 per kid) it "banks" a reward token and fires a mini-game. We started with a popup, hated how it interrupted, and moved to an inline charging bar + a ⚙️ settings menu.
  • Reward mini-games. A rotating set of quick, silly games (we shipped 5: Star Pop, Whack-a-Mole, Memory Match, Catch It!, Bubble Pop) that play for ~15 seconds as a "brain break," then drop back into the quiz. Same games for both kids — the younger one feels grown-up playing the same thing. Build a tiny GameDef registry so adding a game is a data entry, not a refactor. A ?game=<id> URL backdoor is invaluable for testing them without grinding a streak.
  • Themes that rotate on "Next Question" (not on answer — rotating on answer is jarring).
  • localStorage auto-save after every answer: score, streak, best streak, difficulty, category scores. Restore on mount; on corrupt/missing state, reset cleanly. No save/load buttons — it's invisible. Keep one "New Day" reset that also clears storage (doubles as a cache-buster, see §6).

Note: "fun" has no automated test. If you're building with an agent, these are the parts to eyeball yourself with a real kid — the deterministic generators you can prove correct; a game being fun you can't.


6. Hard-won lessons (the stuff a fresh spec won't tell you)

  1. Don't use an LLM for anything code can compute. (§3.) Counting four butterflies is not an AI problem. This is the whole ballgame — cost, latency, and correctness all improve at once.
  2. "Correct" means what the kid sees, not what's technically true. The moon is astronomically grey; it renders yellow on a phone. A unicorn emoji "is" whatever the color bank says; it renders white-and-pink and the kid taps pink. Audit your emoji/color banks against how they actually render on the target device, and drop multi-color / platform-variant emojis (🦄🐝🐧🐞🧢☂️🐬 bit us) entirely.
  3. Watch out for the label spelling the answer. A letter question showing the word "OWL" gives away that it starts with O. Lowercase the displayed word, or don't show it.
  4. Kill trivial formats. "Which letter is W?" with W right there is not a question. Enumerate the formats a generator can emit and remove the degenerate ones.
  5. The bug you "fixed" but the kid still sees is probably a cache. After a real fix, the child hit the old question again — it was a stale service-worker/browser cache, not the code. Verify the deployment, then give yourself a cache-buster (our "New Day" button fetches fresh seeds).
  6. Test on the actual kids, early. Every important bug here was caught by a 5- and 10-year-old using the app, not by us reading code. Ship a rough version to a real child fast.
  7. Prompt caching may not pay off at this scale. We tried it; the model's minimum cacheable prompt size was larger than our whole static prompt, so it never triggered. Don't assume; measure.
  8. Track your cost from turn one. We didn't, and reconstructing "what did this cost to build" after the fact from token logs was painful and mostly impossible. If cost matters to you, capture it as you go.

7. A starting prompt for your agent

Paste this (with your kids' details filled in) after this document. List however many kids you have — one line each, one or ten:

Build the kids' quiz app described in this brief. I have [N] kid(s):

  • [Name], age [N], [grade] — interests: [list]. [reading level / any constraints]
  • [Name], age [N], [grade] — interests: [list]. [reading level / any constraints]
  • (...one line per kid — add or remove lines to match how many you have)

Use [my stack, or "the reference stack in the brief"]. Start by proposing: (a) the per-kid PlayerConfig for each child, (b) which question types are deterministic vs. AI-backed for each, and (c) the question JSON contract. Do not write code until I approve those three. Then build the deterministic generators first (with tests asserting the true answer is always a choice), then the engine, then the AI proxy, then the reward layer. I'll test on my kids between steps.


Built for real, running in production for a household of two kids — a 10-year-old son and a 5-year-old daughter — but the design is the same for one kid or a whole classroom. "Son" and "daughter" and their ages/grades are just examples; replace them with your own kids' real profiles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment