MathTrail

How the service works

MathTrail stands between the chat a family already uses and the parent's Google Drive. The chat's model writes every task, and the service decides whether it reaches the child. It is one program: it calls no model of its own and keeps nothing between requests.

  • One Go binary on Cloud Run
  • Stateless
  • No database
  • No LLM calls of its own
  • MCP 2026-07-28 · MCP Apps

Chat client1 · client

  • Claude
  • ChatGPT
  • other MCP Apps hosts

The chat's model writes the task, and the card shows it to the child.

MCP · tool calls over HTTPS

MathTrail on Google Cloud2 · runtime

Cloud Run · one Go binary · stateless

The core — pure computation

  • The rule
  • Ratings
  • Checks
  • Solver
  • Content

Around it: OAuth, the MCP server, the card, sealing, limits.

Drive API v3 · with the parent's own token

Google Drive3 · storage

MathTrail/mathtrail-profile.json

One file per child, in the parent's own Drive — the only thing kept for long.

01

The map

Who talks to whom. Neither the child nor the adult reaches MathTrail directly — they talk to the chat host. Everything in the middle block is a part of one process, not a separate service. Choose any part to see what it does and what it links to.

Parts of the service

The family's chat

Claude in a browser, on a computer and on a phone · ChatGPT

People

Chat host

MathTrail

One Go binary · Cloud Run · a domain of its own

between the parts, plain function calls

The way in

Tools — read the profile, compute, write it back

Pure computation — no network

Around the core

Google and GitHub

The only services outside the binary

The parent's Google account

Google Cloud

GitHub

Choose a part of the map: everything it links to stays bright, and the rest fades.

The family's chat

Adult

A parent or a tutor. Owns both the chat account and the Google account: connects MathTrail, signs in once, creates the profile and follows the progress. The only one who signs in.

The family's chat

Child

Grades 1–6. Solves tasks on the card, in the adult's chat. Has no sign-in of their own.

Chat host

The host's model

Claude or ChatGPT. Writes the task, a Starlark solver, an explanation behind each wrong option and a self-check; leads the lesson in words wherever cards are not drawn. MathTrail itself calls no model.

Chat host

MCP Apps runtime

Draws the card in a sandboxed frame, passes it the language, the platform and the size, and forwards the card's own calls with the host's token — the card itself never sees the token.

Chat host

Client metadata document

The host names itself by an HTTPS address. MathTrail fetches the document at that address, guarded against SSRF, rather than keeping a registry of clients.

MathTrail · the way in

HTTP router

The only way in: /health, /mcp, the OAuth endpoints and .well-known. Timeouts, a limit on the body's size, an Origin check; the issuer is its own domain, and any other Host gets a 404.

MathTrail · the way in

Authorization server

MathTrail is its own OAuth 2.1 server with PKCE: metadata, a consent screen of its own, sign-in with Google, tokens, renewal and revocation. Everything a server with state would keep is sealed into tokens and cookies.

MathTrail · the way in

MCP server

The protocol: Streamable HTTP 2026-07-28, stateless, on the official Go SDK. It lists and calls the tools, serves the card as a ui:// resource and hands the model its instructions with their version.

MathTrail · the way in

Limits

A pace per address before sign-in and per account after it, and an overall ceiling, all in memory — then the daily ceilings, which the profile counts. Past a limit, a plain sentence the model passes on.

MathTrail · tools

Tools

Eleven tools of one shape: read the profile, compute, write it back. The model calls most of them; the card calls its own — for the task, the answer, the progress and the profile's form. Nothing secret ever goes into a result.

MathTrail · pure computation

The rule

Picks the brief deterministically: after a mistake, the same topic, to consolidate it; otherwise the topic not seen for the longest; the difficulty from the corridor where the chance of success is 70–85 %.

MathTrail · pure computation

Ratings

P = 0.2 + 0.8·σ(θ + δ − β). Elo moves only on whether an answer is right, with a step that shrinks; a chess scale and eleven ranks; a topic counts as mastered by a cautious estimate.

MathTrail · pure computation

Checks

What a handed-in task must pass: its structure and five different options, the letters of the lesson's language, distinct explanations, the solver, the self-check, readability, near-duplicates and the text drawing. Every failure is reported at once.

MathTrail · pure computation

Solver

Runs the model's Starlark program, which tries the options; exactly one must come out right. Limits on steps and time, no network and no files.

MathTrail · pure computation

Content

Everything built into the binary that is not code: the catalogs of topics, traps and skills, the reference tasks, the schemas, the solver templates, the drawing frames and the model's instructions with their version.

MathTrail · around the core

Widget

The card: Preact built into one HTML file that loads nothing from elsewhere, the languages' dictionaries inside it. The layout works from 320 px, and the buttons fit a finger.

MathTrail · around the core

Store

Finds the profile by a hidden mark, compares before it writes, reconciles two tabs, restores the file from Drive's pinned revisions when the parent asks — and keeps within a small budget of Drive calls.

MathTrail · around the core

Sealing

One mechanism, two jobs: it seals the sign-in and Google's tokens into the tokens the host holds, and a task's answer — the current task's and the one written ahead — into the profile. The keys come from Secret Manager and rotate by key id.

MathTrail · around the core

Logging

One structured summary per call: the tool, the outcome, the duration, the instructions version; attempts and the reasons for a refusal as events of the task. No personal data, no task text, no answers.

MathTrail · around the core

Container and configuration

Wires the parts together and closes them in reverse order, works with timeouts and a graceful shutdown, reads all its configuration from the environment — and refuses to start on Cloud Run with development switches.

The parent's Google account

Google OAuth

The parent's sign-in with the drive.file scope alone, renewal and revocation. Revoking any token ends all access.

The parent's Google account

Drive API v3

The only long-term storage: one JSON profile in a visible MathTrail folder of the parent's Drive, read and written with the parent's own token.

Google Cloud

Secret Manager

The sealing keys and Google's client secret: pinned versions, read once when an instance starts.

Google Cloud

Artifact Registry

The image of every revision, by digest; old images are deleted.

Google Cloud

Cloud Logging

Where the service's stdout goes. The lines children are counted by are kept for 62 days in a bucket of their own.

Google Cloud

BigQuery → Data Studio

Counts of use are kept for years and counted again every night; no table holds the name a child is counted under. A private report and a public one, which shows no group of fewer than ten children.

GitHub

Actions

The build, the checks, publishing the image and the deploy through Workload Identity Federation — with no service-account keys.

GitHub

Pages

This site: the topics' pages, and the privacy policy and the terms the consent screen links to.

02

A lesson, call by call

The model and the card call the same tools and stand on one source of truth — the profile's file in the parent's Drive. For a lesson to go on, they have nothing to tell each other.

Scenario
  • a call
  • its answer
  • inside the host

Task

  • Child
  • Card
  • The host's model
  • MathTrail
  • Drive
  1. 1Child → The host's model: “a new task, please”

  2. 2The host's model → MathTrail: next_task

  3. The host draws the card as soon as the call begins: “Preparing the next task…”

  4. limits · the rule picks the topic, the level and the difficulty

  5. 3MathTrail → Drive: write the open request

  6. 4MathTrail → The host's model: the request is open — fetch the package

  7. 5The host's model → MathTrail: get_package

  8. 6MathTrail → The host's model: brief · 3 reference tasks · traps · templates · frames

  9. The model writes the task, a Starlark solver and a self-check

  10. 7The host's model → MathTrail: submit_task — up to 3 attempts

  11. checks · the solver must find exactly one right option

  12. 8MathTrail → Drive: the task becomes the current one, its answer sealed

  13. 9Card → MathTrail: read_task — the card asks every 4 s

  14. 10MathTrail → Card: text · drawing · five options · hint — no answer

Every call reads the profile from Drive as well — those reads are left out. The card that waits never sees the package: its reference tasks carry their answers. While the child solves, the model writes the next task ahead with prepare_task, and when the child asks for another, next_task puts it on a new card at once — without this wait.

Answer

  • Child
  • Card
  • The host's model
  • MathTrail
  • Drive
  1. 1Child → Card: presses C

  2. 2Card → MathTrail: submit_answer — the chat spends no turn

  3. the first answer alone · the seal is opened now · θ and δ move only on whether it is right

  4. 3MathTrail → Drive: write the ratings and the history

  5. 4MathTrail → Card: right or not · the trap behind C · the solution · the rating before → after

  6. 5Card → The host's model: ui/update-model-context — one line on what happened

  7. 6Child → The host's model: “why not 6?” — a question in the chat

  8. 7The host's model → Child: explains, starting from the trap

The press records the answer before anything is explained. If the host's message to the model is lost, the child still sees the result, and the model catches up at its next call.

Sign-in

  • The parent's browser
  • Host
  • MathTrail
  • Google
  • Drive
  1. 1Host → MathTrail: POST /mcp, no token

  2. 2MathTrail → Host: 401 · where the metadata is

  3. 3Host → The parent's browser: opens /oauth/authorize · PKCE S256

  4. 4The parent's browser → MathTrail: authorize

  5. A consent screen of its own: which client, and where it will send the parent back

  6. 5The parent's browser → Google: sign-in with Google · drive.file alone

  7. 6Google → The parent's browser: back, with Google's code

  8. 7The parent's browser → MathTrail: /oauth/callback

  9. 8MathTrail → Google: exchange the code

  10. 9MathTrail → Drive: /about — whose Drive is it?

  11. user id = HMAC of the Drive account · Google's tokens sealed into our code

  12. 10MathTrail → The parent's browser: 302 · code = mt1.c.…

  13. 11The parent's browser → Host: hands the code back

  14. 12Host → MathTrail: /oauth/token · code + verifier

  15. 13MathTrail → Host: access 15 min · refresh 30 days

The parent signs in once. The service keeps nothing of this exchange: the request travels in a sealed state, and Google's tokens in a sealed code.

The current task as a state machine

Every “only once” rule is a field of the profile, checked within the same read, computation and write. Nothing else remembers anything.

nonenext_taskbrief readyrequestedsubmit_task ✓sealed, handed outissuedsubmit_answerrecorded onceanswered

  • requested ↺refused, attempts left — no more than three per request
  • requested → nonethree refusals, or 15 minutes from the request's opening; the day's tasks are not spent, and three refusals count as a failed request of the day
  • issued → requested“Another task” before an answer — a skip is recorded, the rating untouched
  • answered ↺any later answer — from a second tab or from the model — gets the first result back and writes nothing
  • answered → requestedthe next task, on a new card; the one answered is no longer current
  • answered → issuedthe next task was written ahead — next_task puts it on a new card at once, skipping requested
  • issued → nonethe seal can no longer be opened — the key rotated twice; nothing is recorded

03

Where the state lives

Everything a server with state would keep in a database is kept by someone else: in a sealed token the chat holds, in a cookie in the parent's browser, or in the parent's own Drive. Restart the service or start a second copy — nothing is lost.

MathTrail itself

Nothing

No accounts, no sessions, no registrations, no Google tokens. Every request carries all it needs.

The chat's token store

Sealed tokens, opaque to the host

Access token
15 min
Refresh token
30 days, ≤ 90

Sealed inside are Google's tokens, an opaque user id for the limits, and the country the parent signed in from.

The parent's browser

Two cookies, no session

Approved clients
180 days
Sign-in guard (CSRF)
10 min

Losing them costs one more consent screen, or a new sign-in.

The parent's Drive

The only thing kept for long

mathtrail-profile.json
until deleted

The profile, the ratings and the current task, its answer sealed. The parent can open the file, and Drive keeps its history.

Secret Manager

Read once, when an instance starts

Sealing keysafter a rotation, the previous key is still accepted
90 days
Google's client secret
pinned

The instance's memory

Caches alone: lost on a restart, and nothing breaks

Request rate counters
minutes
Where the profile's file is
10 min
Clients' documents
5 min – 24 h

Left out of v1 on purpose

  • A database
  • Queues
  • A session store
  • A shared cache
  • A bank of ready tasks
  • An LLM API client
  • REST and OpenAPI

04

The profile's file

One child, one JSON file: not a cache of some truth on a server, but the truth itself. Every block has a cap, held on every write, so the file never grows without a bound: a usual one keeps within the target, and one near its caps goes past it. Past tasks are kept as fingerprints, not as text.

The file's size

≈ 61 KB / ≈ 79 KB

Which size to show
BlockWhat it holdsWho writes itSize
FingerprintsUp to 200 sketches of the tasks handed out, for the near-duplicate check — no text, no topic, no datenext_tasksubmit_task27 KB / 29 KB
Rating historyWhere θ and the topic's δ stood before each recent answer, and θ and every topic at the start of each of the last 7 days with answerssubmit_answer14 KB / 15 KB
Ratings and topicsθ and where the child started; for each topic: δ, the answers, the traps they fell into, mastery and its levelnext_tasksubmit_tasksubmit_answer8 KB / 14 KB
Recent answersThe last 20 answers and skips — and never fewer than the 5 answers the rule readsnext_tasksubmit_answer4 KB / 5 KB
Current task, sealedThe answer, the trap behind each wrong option, the solution and the solver — one opaque stringnext_tasksubmit_tasksubmit_answer4 KB / 8 KB
Current task, openWhat the card shows: the text, the drawing, five options, the hint — and, after the answer, the answer itselfnext_tasksubmit_tasksubmit_answer2 KB / 4 KB
Open requestWhile the task is being written: its id, the brief, the attempts used and who picked the topicnext_taskprepare_tasksubmit_task1.5 KB / 2 KB
Service and childThe schema version, a random UUID, the revision; the pseudonym, the grade, the interests, the notes; the day's counterssave_profileedit_profileevery write0.6 KB / 1.5 KB

A task written ahead lies beside the current one and weighs about as much. What the file does not hold: the texts of past tasks, the answer in the open until the child has answered, anything that names the parent, and a rank — ranks are computed when they are shown. Drive has no conditional write, so the file is read again before a save, and the write is refused if it has changed: two tabs never silently overwrite each other.

05

Sealed — and for how long

One mechanism does two jobs: it seals the sign-in into the tokens the chat holds, and a task's answer — the current task's and the one written ahead — into the profile. Each seal carries its purpose, so an access token cannot pass for a refresh token, and a sealed answer cannot pass for anything else.

A token, taken apart

mt1.a.kQ7fWx.5rJ0Yb2mQ8vK1nT4pXc9…

mt1
the format's version
purpose
c code · a access · r refresh · d client · s state · k consent · t a task's answer
key id
taken from the key itself, so a rotation goes unnoticed
ciphertext
XChaCha20-Poly1305, a random 192-bit nonce

Not a JWT: no one but the service reads it, and no claim can leak into a log from an opaque string.

Lifetimes, on a logarithmic scale

  • Authorization code
    60 s
  • Sign-in in progress
    10 min
  • Access token
    15 min
  • Refresh token
    30 days, sliding, 90 at most
  • Sealing key
    rotated every 90 days
  • Approved clients
    180 days

A server without state cannot mark a code as used, so the code's window is kept small. After a rotation the previous key is still accepted, so neither a token nor a sealed answer is lost when the key changes.