The technical account: the run model, each stage's real contract, the approval gate, replay, and what "verified" means. Live at loopmachine.coey.dev.
A run is not a synchronous HTTP call that dies with the request. POST /api/runs creates a Durable Object (LoopupRun) with SQLite storage holding the run's identity, its stage table, and every stage's receipt. The DO advances itself with an alarm-driven stage machine: each stage runs, writes its result to SQLite, broadcasts to any watchers, then schedules the next stage via storage.setAlarm(). That's why a run survives the request, can be polled or streamed, blocks for human approval, and can be replayed later.
POST /api/runs {goal}
-> DO LoopupRun created, SQLite row written, alarm set
stage 0 backwards -> receipt -> alarm
stage 1 manyface -> receipt -> alarm
stage 2 temper -> receipt -> alarm
stage 3 deja-shared -> receipt -> alarm
stage 4 airdesk -> status = awaiting_approval, HALT (no alarm)
[human approves / denies] -> alarm
stage 5 approval -> pass only if status === approved
stage 6 mint -> promote composite -> new tool
stage 7 done -> status = completed
Every stage calls another service's live HTTP API with fetch. LoopUp holds adapters and the run engine; it contains none of the five services' internals. If a stage's service is unreachable, that leg is recorded as a real error and marked unproven — the run does not fabricate a result.
| Stage | Live service | What it contributes |
|---|---|---|
| backwards | backwards.coey.dev | Turns the goal into concrete checks; a failed check (verified-disproved) is surfaced honestly, not hidden. |
| manyface | manyface.coey.dev | Races candidate answers; a critic attacks each; returns the winner plus every loser's verdict. |
| temper | temper.coey.dev | Reuses a prior proven tool if one exists for this goal shape; otherwise promotes the winning move into a new one. |
| deja-shared | deja-shared.coey.dev | Injects a prior learned instinct if one exists; records this run's episode (consented, redaction-aware). |
| airdesk | airdesk.coey.dev | Runs the irreversible step to awaiting_signature, then blocks for a human decision. |
At the airdesk stage the run sets status = awaiting_approval and stops scheduling alarms. Nothing proceeds until a human acts:
POST /api/runs/:id/approve — production path, a real WebAuthn passkey signature.POST /api/runs/:id/dev-sign — a clearly labeled non-production demo signer, for visitors without a passkey.POST /api/runs/:id/deny — records denial; the run stays resumable.Approval binds to the exact artifact hash shown. Send a hash that doesn't match the run's approvalArtifactHash and you get 409 approval void. If the underlying result changed, a prior approval is meaningless — you must approve the thing you actually saw.
GET /api/runs/:id?replay=true re-fetches each upstream receipt and recomputes the evidence root. The verdict distinguishes two kinds of leg, because they are not equally re-derivable:
deterministic backwards, manyface
Same input yields the same receipt. Their stored hash must recompute exactly. This is the tamper-evident backbone — if these don't match, something was altered.
stateful temper mint, deja append, airdesk task
Each call creates a new artifact (a new tool, a new memory entry, a new task), so their hash is not re-derivable by anyone. They are verified as recorded from a live call, and the receipt says so plainly rather than faking a match.
verified: true means every deterministic leg re-verified exactly and every leg actually ran. This is deliberately narrower than "all hashes identical," because claiming a stateful leg is hash-stable would be dishonest.
After a fully-approved run, LoopUp takes its own composite evidence root and posts it to temper /api/promote, minting a loopup-orchestration tool whose provenance points at the run that proved the whole stack. A tool built by composing tools. The mint is downstream of the evidence root and is deliberately not folded back into that root — that would be circular.
POST /api/runs {goal} -> {runId}
GET /api/runs/:id -> {run: {...stages, status, receipts}}
GET /api/runs/:id?replay=true -> {run, replay: {verified, evidenceRoot, legChecks}}
GET /api/runs/:id/ws -> WebSocket, streams stage transitions
POST /api/runs/:id/approve {artifactHash} -> 200 | 409 (hash mismatch, void)
POST /api/runs/:id/dev-sign {artifactHash} -> 200 {note: demo signer}
POST /api/runs/:id/deny {reason?} -> 200, run resumable
GET /api/status -> {ok, service, timestamp}
Run status values: running, awaiting_approval, approved, completed, failed, denied.
The page drives progress by polling GET /api/runs/:id every 1.5s, not by WebSocket alone. The backend reaches the approval gate in about 8 seconds — often before a socket finishes handshaking — so polling is the primary driver and the WebSocket is an optional low-latency enhancement.
/api/* and falls through to assets for everything else.manyface currently races a fixed candidate/critic set; it demonstrates selection-by-test, not an open-ended model tournament.