{"slug": "we-built-one-ai-tutor-for-five-completely-different-subjects-here-s-the-routing", "title": "We built one AI tutor for five completely different subjects — here's the routing layer that keeps it from falling over", "summary": "Vararuchi built a single AI tutor that handles five different subjects — general tutoring, exam prep for JEE/NEET/UPSC, language learning, career coaching, and competitive arena mode — by implementing a centralized routing layer. The system uses a feature registry where each of 60+ AI-backed features declares its model chain, cost, and caching policy, with automatic fallback and refund logic. A key lesson was that the mastery system was broken because only a hidden keyword triggered quizzes; the fix was making the feedback loop explicit with a visible button.", "body_md": "Most \"AI tutor\" apps are a chat window bolted onto a system prompt. We wanted something that actually holds state — knows what you've mastered, adapts difficulty, remembers you tomorrow — across five very different domains: a general subject tutor, exam prep for India's JEE/NEET/UPSC (each with its own official syllabus and negative-marking rules), a language-learning track that goes CEFR A0 through C1, an AI career coach with mock interviews, and a competitive \"arena\" mode.\n\nThat range turned out to be the actual engineering problem. Not \"can an LLM explain photosynthesis\" — every model can do that — but: what happens when you have 60+ distinct AI-backed features, each with different cost/quality tradeoffs, and one of them starts failing at 2am?\n\nEarly on, model calls were scattered — each feature picked its own model, its own retry logic (often none), its own idea of what \"thinking budget\" meant. A cost audit found thinking tokens (billed at the output rate) were the single largest leak in the whole system. That's what forced the redesign.\n\nNow every AI-backed feature is one entry in a single registry file:\n\n`\\`\n\n`js`\n\nexport const FEATURES = {\n\nchat_message: { sparks: 2, chain: LITE_FIRST, stream: true },\n\ngenerate_quiz: { sparks: 1, chain: LITE_FIRST, cacheTtlMs: 30 * 60_000 },\n\nkm_build_concepts: { sparks: 8, chain: FLASH_FIRST },\n\n// ...60+ more, one line each\n\n};\n\n\\`\\`\n\nEach entry declares its price, its model *chain* (primary + fallbacks), and its generation config. The router walks the chain on failure:\n\nIf a cheaper step in the chain ends up answering, the router refunds the price difference automatically. Response caching is an *allowlist*, not a heuristic — only deterministic-ish generators (quizzes, reviews, curricula) get a TTL; personalized chat is explicitly never cached. And the client-side paywall keeps its own copy of the prices — a test asserts the two stay byte-identical, so drift is a CI failure instead of a support ticket.\n\nNone of this is exotic. It's the boring, unglamorous plumbing that every AI product eventually needs and almost none ship on day one — because day one is about the prompt, not the failure mode of the prompt.\n\nHere's the one that stung. We had a whole \"mastery\" system — spaced repetition, progress bars, the works — and users' mastery scores just... never moved. Not broken-looking, just flat.\n\nThe cause: the *only* thing that triggered a quiz (and therefore a mastery write) was the user typing a hidden keyword like \"quiz\" or \"check\" into chat. Practice mode and Review mode — the two surfaces actually designed for this — had no write-back path at all. The UI was doing real work and throwing the result away.\n\nThe fix wasn't a clever algorithm, it was making the invisible contract visible: a shared `masteryRules\\`\n\n+ `pathProgress\\`\n\nengine, and an actual **\"Check my understanding\"** button instead of a magic word nobody was told about. The lesson generalizes past this one bug — if a core feedback loop depends on the user knowing an undocumented trigger, it doesn't exist.\n\nThe other pattern worth mentioning: for a while, every AI call that errored was caught and swallowed — `console.error(e)\\`\n\nand render `null\\`\n\n. From the user's side, that's just a blank screen with no explanation, indistinguishable from the app being broken. We replaced every one of those call sites with a shared error-state component that actually tells the user what happened and gives them a retry path. Small change, but it's the difference between \"this app is broken\" and \"this app hit a snag\" in the user's head — and those get very different reactions.\n\nIt's live at [vararuchi.com](https://vararuchi.com) if you want to poke at\n\nI'd genuinely rather have three people tell me what's broken than a hundred silent signups, so — try to break something, and tell me what you find.", "url": "https://wpnews.pro/news/we-built-one-ai-tutor-for-five-completely-different-subjects-here-s-the-routing", "canonical_source": "https://dev.to/devadath_hk/we-built-one-ai-tutor-for-five-completely-different-subjects-heres-the-routing-layer-that-keeps-15f4", "published_at": "2026-07-20 20:30:00+00:00", "updated_at": "2026-07-20 20:47:27.681246+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-products", "ai-infrastructure", "developer-tools"], "entities": ["Vararuchi", "JEE", "NEET", "UPSC", "CEFR"], "alternates": {"html": "https://wpnews.pro/news/we-built-one-ai-tutor-for-five-completely-different-subjects-here-s-the-routing", "markdown": "https://wpnews.pro/news/we-built-one-ai-tutor-for-five-completely-different-subjects-here-s-the-routing.md", "text": "https://wpnews.pro/news/we-built-one-ai-tutor-for-five-completely-different-subjects-here-s-the-routing.txt", "jsonld": "https://wpnews.pro/news/we-built-one-ai-tutor-for-five-completely-different-subjects-here-s-the-routing.jsonld"}}