at play · case study

The Hub

An AI coding agent on my own server, driven from my phone — any CLI agent by design. What began as an idea tester now runs as production infrastructure: it works nights, runs goal-driven loops, ships products, watches itself, and backs itself up.

hub infographic — a phone with three tabs reaches a self-managed VPS only through a private tailnet; on the VPS, chat and cron bill the API key, the terminal bills the subscription, and the dash watches systemd, docker and CIhover to inspect
role
design & code · ops
stack
FastAPI · Claude Agent SDK · xterm.js · Tailscale
frontend
vanilla JS PWA — no build step, vendored deps
runs on
a self-managed VPS — systemd, isolated users
status
private build, in daily use

Everything it does

Rather touch it than read it? An interactive playground — the interface with example data, a replay of the factory shipping a product, and the loop drawn out — lives on the studio site.

0third parties in the traffic path
2billing paths, kept apart by design
65HTTP + WebSocket routes behind one auth middleware
3terminal sessions that survive a dropped connection
7encrypted backups rotating at any moment
1install command per isolated instance
4pipeline stages, a judge at every gate
6independent ways the goal loop can stop

Born on a walk

The need: ask an LLM something from the phone, mid-walk, without a third-party service reading the traffic. So the agent lives on a server I run, and the phone carries only a thin, installable PWA — a thought typed on the street becomes a running experiment on the VPS before the walk is over. Provider-agnostic by design: another CLI agent is a lane in the diagram, not a rewrite.

The path

There is exactly one road to the server: the tailnet. hub binds to localhost and is published only through Tailscale's private HTTPS — it does not exist on the public internet, there is no relay, no vendor bridge, no one in the middle.

phonehub · PWAtailnet · WireGuardprivate HTTPS, device-to-devicethe public internet is not invitedVPS — hub servicesystemd · own linux userchat → Agent SDKAPI key · meteredterminal → claude CLIOAuth · subscriptioncron jobs → SDK, headlessAPI key · metereddash → systemd · docker · CIread-onlythe PTY strips the API key from its env — the CLI can only bill the subscription
One request's whole journey — phone to VPS inside the WireGuard tunnel, then onto one of four lanes, each wearing its own billing tag.

Two bills, kept honest

The provider's terms draw a hard line: SDK traffic bills the metered API key; subscription OAuth belongs to the first-party CLI only. hub is architected around that line — chat and cron pay per token, the terminal's CLI rides the subscription, and its PTY deliberately strips the API key from the environment so the two paths cannot cross even by accident.

Reading the compliance page before drawing the architecture is the whole trick — the billing split isn't a setting, it's the shape of the system.

Locked from both sides

The tailnet is the outer wall; hub carries its own auth as a second, independent layer — scrypt passwords, session tokens persisted only as hashes, per-IP lockout. Auth is middleware, not a per-route decorator: a route added next year is protected because it exists. And there is no loopback bypass — "local" on a shared box is not "me".

The kill switch exists — but it demands a restart, logs on every boot, stamps every response header, and paints a red banner. An off switch nobody can see is how a box stays open for months.

The night shift

Ideas arrive by day; the server has all night. The night is a schedule, not a place: any stage of any project can be marked "tonight" from its card, and the sweep runs it in an isolated git worktree — never the live checkout, never main — while a small judge model reads the actual branch diff against the stage's brief. The morning brings a push, a written report, and a decision that stays human: accept, continue, or discard.

by daystages marked"tonight" on theproject card01:00 — the night sweepheadless agent, one task at a timeisolated git worktreefresh branch night/<id>never the live checkout, never mainthe judgea small model readsthe actual branch diff~$0.001 per verdictmorningpush + reporton the phoneretry, with the judge's feedback · or blocked, explained
The overnight loop — day queue, isolated worktree runs, a verdict grounded in the diff instead of the agent's own optimism, and a morning report.

The loop closes

Work is a chain, not an errand: an idea walks research → plan → spec → build, every stage commits its artifact into the repo — IDEA.md, RESEARCH.md, PLAN.md, SPEC.md — and a judge gates every stage, so the chain stops at the first result that doesn't hold. The model forgets everything between runs; the repo doesn't. Above the chain runs the goal loop: a goal stated once, and cycles that keep going until a checker model — grading the project against the goal, not the agent's account of it — says it is met.

GOAL.mdstated once,lives in the repomet → stop · reportcheckgrades the projectagainst the goalone next stepnamed by thechecker, not by meworkerruns exactlythat stepjudgegrades theattemptround again — cycle ceiling 8, enforced even if asked for moreit ends on: goal met · cycle cap · judge blocked · a cycle with no commits · night's end · me pressing stopthree separate models — the worker, the judge, the checker — and nobody grades their own work
One cycle of the goal loop — the checker decides met or not met, names the single next step, and the return edge carries a hard ceiling. Stopping is designed first; running forever is the failure mode, not the feature.
A loop that cannot stop is a bill, not a feature. Six independent conditions end it, and the cycle ceiling is enforced per stretch — even a hand-edited config cannot push past it.

A factory at the end of the line

One command provisions a complete product — repo, CI, hosting, database, domain, analytics — then the agent builds inside it, and a product can ship the same day. The lifecycle registry closes the loop: after each product's review window it reads real traffic and answers the question dashboards dodge — is 22 visitors good or bad. Every product under Eldora Studio came off this line.

new-product <slug>one command, live the same dayrepoCIpagesD1 dbdomainanalyticsdash cardlifecycle…then the lifecycle registry reads real traffic after the review window and answers:killno visitors, no signal — tear down by checklistwatcha pulse — leave it live, check againkeepreal use — investkilled products stay in the registry on purpose — the record is the point
The provisioning line and the reckoning after it — teardown is a checklist that names the dangerous half-states instead of quietly building them.

It runs itself

A watchdog engineered against alert fatigue — pushes only on state change, two failures before an alarm, recovery announced immediately. Every night an encrypted backup gathers everything git doesn't hold; restore depends on nothing but openssl. The day starts with a briefing push: what ran, what failed, what the night shift built.

A server of one's own

Everything under hub is handled solo, and repeatably: one install script provisions a full isolated instance, so a second hub is one command on any box. The same screen that talks to the agent watches the machine it lives on — ops isn't outsourced here; it's part of the build.

Anthropic has since shipped the mainstream version of this idea — Remote Control, bridging a laptop session to the phone. hub still runs where that can't: on a server that never sleeps, over a network nobody else can see.