Project overview · local teaching tool
A chat app for learning the Claude API by watching it work — every request, every response, and what each one cost.
The premise
A normal chat window with an activity log beside it. The log shows every request that leaves the machine and every response that comes back — the actual JSON, not a summary — along with what each one cost and why it happened.
You can switch the orchestration pattern, the models, the tools, the system prompt, caching, thinking and effort between messages, and watch the numbers change. It is built for people preparing for the Anthropic certifications, and for anyone who would rather see stop_reason: "tool_use" arrive than read about it.
It runs entirely on one machine. No database, no Docker, no hosting. The only network calls go to the Anthropic API.
Asked what stop_reason: end_turn means, the Terse system prompt answered in 23 output tokens. Teaching assistant took 589 — 25× the cost for the same fact. The system prompt is the cheapest lever in the app and the easiest to overlook.
Node · Express · vanilla TypeScript · no frontend framework
| Package | Version | Role |
|---|---|---|
| @anthropic-ai/sdk | 1.8.0 | The Claude API client. One shared instance, with baseURL pinned to api.anthropic.com |
| express | 5.3.0 | HTTP server and static file host — 17 routes |
| zod | 4.6.7 | Schemas for structured outputs via output_config.format |
| @modelcontextprotocol/client | 2.4.0 | Connects to an MCP server, so its tools join the Tools tab beside the local ones |
| dotenv | 18.0.10 | Loads an optional .env key at startup |
| Package | Version | Role |
|---|---|---|
| typescript | 7.0.2 | Two configs — one per half of the app |
| tsx | 4.23.15 | Runs TypeScript directly, restarts on save. The normal way to use the app |
| @types/node | 26.6.6 | Server-side type definitions |
| @types/express | 5.0.6 | Route and middleware types |
| Node.js | 20 or newer required; developed on 24.4.1 |
| Module system | "type": "module" — ESM throughout, which means .js extensions on imports of .ts files |
| Frontend | Hand-written TypeScript compiled to ES modules. No React, no bundler, no CSS framework |
| Default model | claude-opus-5-5, with claude-haiku-5-5 as the small model |
The split is a security boundary, not a build convenience. The server config targets ES2022 with module: NodeNext, strict: true, and types: ["node"]. The client config sets "types": [] — deliberately empty — so server-only globals like process cannot be referenced from browser code even by accident. The API key is never reachable from the frontend half.
The page is the teaching surface. A framework would put its own abstraction between the reader and the JSON on the wire, which is the one thing the app exists to show. The Markdown renderer is hand-written for a related reason: model output renders through textContent, never innerHTML, and a packaged renderer would not guarantee that.
Patterns, tools, controls
| Pattern | Requests | When it earns its cost |
|---|---|---|
| Single call | 1 | The baseline every other pattern has to beat. Does not run tools |
| Tool loop | 2–4 | Anything the model cannot know: the time, a calculation, a file |
| Prompt chaining | 2 | Long structured answers — a plan call, then a writing call |
| Routing | 2 | Mixed question types one system prompt cannot serve |
| Parallelisation | 4 | Several angles that all matter. Buys latency, not money |
| Orchestrator + workers | 3–7 | Separable parts, where what the parts are depends on the question |
| Evaluator + optimiser | 2–6 | When being wrong is expensive and being slow is not |
| Tool | Runs on | What it teaches |
|---|---|---|
| calculator | client | The basic loop. Arithmetic the model would get wrong |
| get_current_time | client | Something the model genuinely cannot know |
| notes | client | State across calls, and multi-step loops |
| search_docs | client | Retrieval over a local corpus |
| read_file | client | Sandboxing — resolve the path first, then check it is still inside |
| flaky_service | client | Controllable failure. failRate: 1 fails every time |
| web_search | server | Runs on Anthropic's infrastructure. No loop, no client code |
| System prompts | Five — terse, teaching, exam, beginner, none |
| Models by role | Deciding, producing, judging — assigned separately, because a classifier does not need the big model |
| Prompt caching | off, prefix, full |
| Effort | Five levels via output_config.effort, which replaced budget_tokens |
| Extended thinking | Display only — thinking is always on and always billed on these models |
| Tool choice | auto, none, and two forced options current models reject with a 400, left in so the error is visible |
docs/build-plan.html · 68 steps · 10 phases
The project was built from a self-contained build plan that sits in the folder it describes. It is one HTML file with its own styling, dark mode, a progress counter, and checkboxes that persist to browser storage. Each step is sized for under 30 minutes and ends with a done-check you can run.
What to do, a done check — a literal command plus its expected output, such as npx tsx scripts/check-env.ts printing PASS — sk-ant-a… — and often a gotcha recording a trap hit in practice. The plan tells you that if a step stalls, paste it back to Claude and ask for it to be split into 23a, 23b.
tsc fails with TS18003 when include matches zero files — create the folders and one throwaway file first..js extensions on imports even though the file on disk is .ts.ANTHROPIC_BASE_URL, so a company gateway silently intercepts the call.process.exit() ends the program, so narrow the key with an early return instead.What the code enforces on itself
Each file opens with a comment saying what it does and why it exists; npm run audit-comments checks none were missed. These record design history rather than restating the code. The tool registry's header explains that an earlier version put the interface in the registry file, creating an import cycle that worked with one tool and threw Cannot access 'currentTime' before initialization with two.
Tools follow three levels: types.ts imports nothing, definitions/ imports types, registry.ts imports the definitions. A module defining shared types belongs at the bottom of the graph. The frontend holds a matching rule in the other direction — app.ts imports from log.ts, settings.ts and transcript.ts, and nothing imports app.ts.
.env can seed it at startup.tsconfig sets "types": [] so server globals cannot be referenced from frontend code.textContent, never innerHTML — including inside the hand-written Markdown renderer.read_file resolves to a canonical absolute path then checks it is still inside the sandbox, the ordering that defeats ../.Verified 11 November 2026
The pricing rates are hard-coded in server/usage.ts and will go out of date. The cost figures are for noticing that one pattern costs four times another, not for billing.