Skip to content

~12 min readgrounded in apps/web/app/api/**/route.ts (65 files, each read for its exported verbs, header comment, body destructuring and runtime) · packages/contracts

HTTP API·

Sixty-five route files under apps/web/app/api/. Each row below is read from the file: the verbs it exports, its header comment, and the fields it destructures from the request. The app is a thin front for the worker — most routes validate, attach identity, and proxy to an internal worker endpoint with a 10 s bound; a hung worker degrades to 503 {error} rather than an opaque 500.

How to read the tables·

Auth is one of:

Label Means
session a tiny_session cookie (browser) or Authorization: Bearer <cli token> — the same getSession everywhere. Missing → 401 {"error":"login required"}
device {deviceId, token} in the body; no session. The worker resolves the owner from the token hash, and any userId in the body is ignored. Off the per-IP limiter
internal X-Internal-Key from the worker's cron — not for clients
public anyone; usually IP-rate-limited when it costs something
owner session and OWNER_LOGIN

Runtime: routes run on Vercel's Edge unless marked Node — those are the ones that need > 25 s to first byte or new Function/node:crypto.

Bodies are JSON unless stated. ? marks optional fields.

Health·

Route Auth Returns
GET /api/health public {ok, service:"web", workerConfigured, appUrlConfigured, paymentsEnabled} — booleans only, never values

Identity·

Route Auth Body / query Returns
GET /api/auth public — redirect to GitHub authorize; state = <nonce>:<path> with the nonce in an httpOnly cookie (login-CSRF guard)
GET /api/auth?code= public GitHub callback upserts the user in D1 via the worker, sets tiny_session, redirects to path
GET /api/me session — the user + owned tinys + reputation standing; 401 when logged out
GET · POST /api/logout any — clears the cookie
GET /api/auth/webauthn/register session — registration options; challenge in a signed httpOnly cookie
POST /api/auth/webauthn/register session attestation verifies, stores the credential in D1
GET /api/auth/webauthn/login public — authentication options (discoverable / usernameless)
POST /api/auth/webauthn/login public assertion verifies against the stored public key, issues a session
POST /api/auth/cli session {port, state} or app scheme mints a 5-minute aud:tiny-cli-code JWT and redirects to 127.0.0.1:<port> (or the tinyapp scheme — an allowlist, never a client URL)
POST /api/auth/cli/token public {code, state} {ok, token, user, expires} — a 90-day aud:tiny-cli JWT accepted as Bearer by every session route
GET /api/udid?profile=1 public — a .mobileconfig Profile Service payload
POST /api/udid public (the iPhone) PKCS7-signed plist stores the UDID, 301 to /ios/registered.html?udid=
GET /api/udid?list=1 · ?count=1 owner — enrolled UDIDs / their count

The whole flow is explained in Identity.

Tinys·

Route Auth Body / query Returns
POST /api/tiny public, IP-limited {name, key?} the tiny's public record {name, private, active, systemPrompt…}; the owner's session, or the right key for a private tiny, unlocks the full config incl. MCP headers. Missing → {active:false}
POST /api/login public, IP-limited {name, key?} {name, ok, private, active…} — the private-tiny key check; the IP window is the brute-force budget, so no per-user widening
POST /api/control session {name, systemPrompt?, systemKnowledge?, data?, key?, hook?, priv?, worker?, schema?, skills?, mcpServers?, hero?, theme?, logo?, intro_vibe?, chips?, tagline?, voice?} create or save a tiny; the worker authorizes by ownership (userId match)
DELETE /api/delete session {name} permanently removes a tiny you own
POST /api/worker public, IP-limited {name, worker} fetches the URL you supply (size-bounded), {message:"Worker is active.", schema} — this is the route that fetches a caller-supplied URL, hence IP-keyed
GET /api/manifest/[slug] public — a per-tiny PWA manifest; icons are square with true sizes (a mismatch has crashed Chrome's browser process)
POST /api/visit public, IP-limited {name} pageview beacon → owner notification, with the visitor's identity when signed in
GET /api/follow?login= session — {following}
POST /api/follow session {login, action?: "follow" \| "unfollow"} the follower is always session.sub; follows are public edges
POST /api/share session, limited {name, messages[]} {id, url} — KV-backed share
GET /api/share?id= public — the shared conversation
DELETE /api/share session {id} removes your share

The agent loop·

Route Auth Body / query Returns
POST /api/chat session or public (free tier) {messages:[{role, content}]} + headers x-tiny-name, x-tiny-session, x-tiny-key, x-tiny-system-prompt, x-tiny-mcp-servers, x-tiny-metadata, x-tiny-model-{provider,id,api-key,base-url,region,max-tokens,additional-fields} an SSE stream of typed frames ending in [DONE] — The agent loop has the frame table
POST /api/chat/tool-result session {toolUseId, payload} {ok} — a device returning the outcome of a client-side tool (media included) so the model sees it
POST /api/run-tool Node internal {action:"validate", code} · {action:"run", code, args} {ok} / {ok, result} / {ok:false, error} — the sandbox the Edge chat route cannot host
GET /api/tools session — {ok, tools:[{name, description, params, code, created}]}
POST /api/tools session {name, description, params?, code} forge a tool (sandbox-validated)
DELETE /api/tools session {name} {ok}
POST /api/tools/run session, per-user limited {name, args?} {ok, result} — run one of your forged tools; what the tiny-vercel MCP server exposes
POST /api/tools/install session {login, name} {ok, name} — copy another builder's public tool into your box, re-validated
GET · POST · DELETE /api/tools/trust session {owner} trusted GitHub owners for install_tool (cap 20) — a user action the model cannot take itself

Devices·

Route Auth Body / query Returns
GET /api/devices session — {ok, devices:[{id, name, kind, online, last_seen, capabilities, lan_url, url}]}
POST /api/devices session {name, platform?, kind?, capabilities?} or {name, kind:"endpoint", url, secret} {ok, device_id, device_token} — the token appears once; endpoint devices get no token
DELETE /api/devices session {deviceId} {ok} — instant revoke
POST /api/devices/adopt session {deviceId} {ok, device_id, device_token} — rotates the token for a device you own; the old one stops immediately
POST /api/devices/heartbeat device {deviceId, token, capabilities?, lanUrl?, wantUnread?} {ok, unread?} — presence for 60 s
POST /api/devices/relay session {toDevice, payload} (JSON ≤ 8 KB) {id}
GET /api/devices/relay?inReplyTo= session — {reply?}
PUT /api/devices/relay device {deviceId, token, max?} {ok, messages:[{id, payload, created_at}]}
PATCH /api/devices/relay device {deviceId, token, inReplyTo, payload} {ok}
POST /api/devices/event device {deviceId, token, kind, detail?} onto the owner's event ring; kind allowlisted
POST /api/devices/task-result device {deviceId, token, taskId, summary?, result?} deposited under a task_* ticket + one push
POST /api/devices/transcript device {deviceId, token, text, label?, audioUrl?, durationS?} stores on-device transcriptions past the relay sweep
GET /api/devices/transcript?id=&limit= session — a device's transcripts
POST /api/devices/ask Node device {deviceId, token, text \| audioUrl, tiny?, stream?} {text, card?} — an owner-scoped agent turn
POST /api/devices/messages device {deviceId, token, op:"send"\|"inbox"\|"thread"\|"unread", to?, body?, attachments?, with?, limit?} the DM rail for a device with a keyboard
GET /api/devices/endpoint?deviceId=&action=telemetry\|snapshot session — {ok, result} or image bytes (GET so it works as <img src>)
POST /api/devices/endpoint/chat Node session {deviceId, prompt} {ok, result} · {ok:false, error, unreachable\|timeout\|unauthorized} — one 90 s agent turn on an endpoint device
POST /api/firmware/manifest session {channel, version, url, sha256} {ok} — point a channel at an artifact (a pointer, not a copy)
GET /api/firmware/manifest?channel= session — {bundle?}
PUT /api/firmware/manifest device {deviceId, token, channel} {bundle?} — what a board polls

Devices · Bring your own device.

Memory, graph, settings·

Route Auth Body / query Returns
GET /api/learnings?q=&limit= session — {learnings, relevant?, total} — q is semantic recall
POST /api/learnings session {content} (≤ 2000 chars) store one memory
DELETE /api/learnings session {id} · {scope:"all"} close one (bitemporal) · erase everything + purge the index — a blank id is refused with 400
GET /api/graph session ?node=&hops=&rels= · ?all=1&include_closed= · ?conflicts=1 · ?social=<node> · ?feed=1&limit= subgraph · whole fact graph · contradiction candidates · public social edges + trust · fresh facts from followed builders
POST /api/graph session {keep, close[]} resolve a conflict
GET · POST · DELETE /api/archives session ?id= · {tiny, messages} · {id} {archives} · archive JSON · {id} — rebuilt server-side so credential redaction always runs
GET /api/prefs?key= · POST session {key, value} {ok, value} — key allowlist (theme…); empty value clears
GET · POST /api/model-config session {config} {ok, config} — never the API key, only hasKey; omit apiKey to keep, "" to clear
GET /api/model-providers · ?full=1 session — {ok, providers:[…hasKey, isActive]} · with decrypted keys — the cross-device sync read for your own devices
POST · DELETE /api/model-providers session {provider, modelId?, baseUrl?, region?, maxTokens?, additionalFields?, apiKey?, isActive?} · {provider} upsert one provider · remove
GET · POST /api/account-voice session {voice} {ok, voice} — the account-wide default live-call voice

Notifications, messages, jobs·

Route Auth Body / query Returns
GET /api/events?sinceId= session — the activity ring (200-cap); a worker outage is surfaced, not read as "nothing yet"
GET /api/push session — {key} — the VAPID public key from the worker
POST /api/push session a PushSubscription stored for this user
DELETE /api/push session {endpoint} removed
GET /api/messages · ?with= session — inbox with unread counts · one thread (marks read)
POST /api/messages session {to, message, attachments?} send; to = @login, login or tiny slug; ≤ 4 attachments from /api/media
DELETE /api/messages session {id} delete a message you sent
GET /api/telegram session — {bot:{tiny, allowedChats, enabled, token(masked)} \| null}
POST · DELETE /api/telegram session {token?, tiny?, allowedChats?, enabled?} configure · disconnect
GET /api/jobs session — {jobs, runs}
POST /api/jobs session {tiny?, name, prompt, schedule? \| run_in_minutes?} create
DELETE /api/jobs session {id} delete
POST /api/job-run Node internal the job, from the cron one non-streaming agent turn with the owner's full capability set

Notifications & Web Push · Scheduling jobs.

Voice·

Route Auth Body / query Returns
POST /api/voice/session session {tiny} {sessionId, wsUrl, ticket} — BYO OpenAI key only; the key goes to the Durable Object, never the browser
GET /api/voice/sessions session — your sessions, recent first
POST /api/voice/tool session {name, args} the tool's result — an allowlist of server tools (memory); money movers and tiny CRUD stay chat-only
GET /api/voice/replay/[id] session, owner of the record — the D1 row + R2 asset URLs; fails closed on an unowned row
GET /api/voice/recording-status/[id] session, owner — why a recording would not play (the worker's 409/413/404/424 body, made same-origin readable)

Location and media·

Route Auth Body / query Returns
GET /api/location public — {ok, me, pins:[{userId, login, name, avatar, lat, lng, speedKmh, heading, updated}]} — pins are opt-in data
POST /api/location session {lat, lng, speedKmh?, heading?, accuracyM?} {ok} — beating is the opt-in
DELETE /api/location session — {ok} — the pin vanishes
POST /api/media session or device {data (base64), contentType} · + deviceId, token {key, url} — R2 under an unguessable key, served from the worker's /media/:key

Money — 404 until PAYMENTS_ENABLED="true"·

Every route here answers 404 before touching the worker or a chain unless payments are on for both app and worker. Payments.

Route Auth Body / query Returns
GET /api/wallet session — {ok, balance_micro, history}
POST /api/wallet session {action:"set_price", resource, price_micro} · {action:"pricing", resource} set/clear a price · public lookup
POST /api/wallet/faucet Node session — one drip per UTC day on a self-hosted chain: ledger first, then mint
POST /api/wallet/withdraw Node session {amount_micro} atomic ledger debit → signed USDC transfer to your linked address (never a body field) → recorded, or refunded
POST /api/x402/chat/[slug] public {message} (+ X-PAYMENT on retry) 402 + PaymentRequirements, then the answer after settle-before-serve; free tinys answer without the dance
GET /api/x402/chat/[slug] public — the price/requirements
POST /api/x402/pay Node session {url \| to, message \| prompt, max_spend_micro?, prior_quote?} a quote — no money moves
PUT /api/x402/pay Node session {quote, message \| prompt} execute: debit the ledger, sign, settle — the only outbound money-moving path
GET /api/chain/join · ?format=genesis public — how to run a node · the raw genesis
GET /api/chain/status Node public — the explorer's facts as JSON, spans and limits clamped
GET /api/erc8004/registration/[slug] public — the ERC-8004 registration file for a public tiny; private tinys 403