skip to content

case study · 02

italki-cli

verified

The API that doesn't exist, built anyway.

A personal CLI + MCP server for italki — teacher search, scheduling, reviews, booking — reverse-engineered from the public web API. Local software, in daily use, built so agents can operate it too.

the problem

A whole product behind a web UI

italki has no official API. Searching teachers, comparing prices, checking a schedule, booking a lesson — all of it lives behind the website, unscriptable. I use the platform for my own lessons, and I wanted my tools and agents to reach it the same way they reach everything else: structured calls, JSON out.

The catch with undocumented APIs: nothing behaves the way the payload names suggest.Filters nest inside unrelated keys. Sort parameters silently do nothing. The only honest way to build on it is to send real requests and write down what actually comes back.

the shape

Four layers, one direction

Each layer may only import from the ones beneath it. Add a new surface — REST, a TUI — and nothing below it changes.

services/

fetch italki, return raw — pure functions, no console, no classes

transforms/

raw codes → domain objects: cents→dollars, tag codes→names, 15-min units→minutes

presenters/

domain objects → ANSI text for humans

commands/ + mcp/

CLI args and JSON-RPC tools — thin surfaces only

command parses args → service returns raw → transform translates → presenter formats / json out

the calls

Decisions, and what they beat

01

Zod schemas as the contract

rejected · Hand-written TypeScript interfaces

The undocumented API has 50+ nested fields — verified: 99 tag codes, 7 categories, 13 filters. Types are inferred from schemas, so the types can't drift from reality.

02

Architecture enforced by custom lint rules

rejected · Layer boundaries documented in AGENTS.md

Five oxlint rules (no cross-layer imports, no classes in services, no console in services…) make violations build errors. The diagram isn't aspirational — it cannot rot.

03

Every API behavior verified by request

rejected · Assuming documented-sounding behavior

No official docs exist. `sort_by` accepts 12 values — all return identical order, so sorting is client-side. `page_size` max is really 99, not 20. api-reference.md is curl-truth, not guesses.

04

Three runtime dependencies

rejected · The usual CLI pile: clack, chalk, hono, ora, playwright

zod + citty + the MCP SDK, that's all. Login is pure HTTP with an AES signature — no browser automation. Every rejected dep is a named call, not minimalism for its own sake.

05

MCP returns translated domain JSON

rejected · Raw API passthrough to agents

Agents need to reason in dollars, tag names, and minutes — not cents, CA005, and 15-min units. `text: true` opts into compact output for simple queries.

13

mcp tools on one service layer — search → book → confirm

50+

api response fields validated at runtime

5

architecture rules enforced as build errors

3

runtime dependencies: zod · citty · mcp sdk

proof

The architecture is checkable

One command proves the layering claims: TypeScript strict plus five custom oxlint rules. Violations aren't review comments — they're errors. Captured live while building this page.

live capture · bun run verifyexit 0 · 83ms
$ bun run verify
$ tsc --noEmit && oxlint

Found 0 warnings and 0 errors.
Finished in 83ms on 68 files with 101 rules using 14 threads.

honestly

What's verified vs. what isn't

  • Booking actions are HAR-verified, not live-fired — the code matches captured booking/reschedule/confirm traffic exactly, but the real POST hasn't been sent from the CLI yet. That's on the record, not hidden.
  • An unfiltered --all search takes ~15s — 44 pages fetched concurrently, Zod parsing ~4,300 records. It needs progress output or caching; today it's just honest about the wait.
  • The REST layer the separation rule was designed for doesn't exist yet — it's additive by construction, but that claim is unproven until it ships. YAGNI said wait; the cost is the rule stays untested on that axis.

More case studies as the lab ships.

← back to selected work