Implementation spec · partners · 2 Sep 2026
How the machine runs
Everything below sits under SPEC.md (rules) and IMPLEMENTATION.md (gap analysis). This page is the engineering view: what a backer's dollar does, how a Gauntlet Loop survives being hosted, what the creator actually gets to touch, and what ships first. For product status, phases and next steps see /mvp; for voice, vocabulary and visual identity see /brand.
01 · Three layers, one rule
A Gauntlet Loop is an overseer (the prompt: split the goal, builder + fresh-context critic per piece, compare against a real bar, keep looping) running inside a harness (Claude Code: real subagents, files, shell, headless Chromium, live workbench). Loopfund adds a third layer, the platform: money, the VM, the wave supervisor, observability, publishing.
NUDGES.md, next-wave prompt). It never prescribes decomposition, never lets the builder grade itself, never caps rounds.
fresh context, no Edit] BU --> ART[artifact: repo, playable, shots] CR -- inspects pixels, blind A/B vs bar --> ART CR -- biggest gap --> BU L --> WB[workbench/index.html + WAVE.json] end subgraph P[Platform · Loopfund] LD[loopd wave supervisor] KEY[OpenRouter key
limit = pot − reserve] NUD[NUDGES.md at wave start] PUB[snapshot → CDN, GitHub Release] end O --> L LD -- starts each wave --> L KEY -. bills every call .- H NUD -.-> L WB --> PUB
| Invariant | How the platform keeps it | What would break it |
|---|---|---|
| Goal, not implementation | Run prompt = creator goal + bar + a 12-line reporting addendum. No workstreams, no architecture. | Wizard fields that become a task list; backer "add workstream" UI. |
| A real, inspectable bar | Step 05 requires uploaded references stored read-only at bar/. No bar → first wave's job is to find one. | Text-only bars; letting the builder redefine the bar. |
| Builder never grades itself | Critics are subagents (fresh context by construction) and a PreToolUse hook denies Edit/Write when agent_type matches critic*. | Routing critics to a cheaper model; showing critics the builder's transcript. |
| Unbounded until a human (or the pot) stops it | External wave supervisor loopd starts the next turn after every wave. Stop = pot empty, creator Pause/Ship, plateau checkpoint answered "stop". | Using Claude Code's Stop hook as the loop (capped at 8 consecutive blocks). |
| Watch without interrupting | Workbench snapshotted to CDN; nudges land only at wave start. | Chat piped into the live session; a TTY for backers. |
02 · Funding: wireframes and money flow
Backers buy prepaid compute earmarked for one loop. Loopfund is merchant of record. The creator's withdrawal is $0. Copy uses held for this loop, never escrow (decided 2 Sep; legal meaning implies a fiduciary we are not). Charged immediately at pledge so the pot is real money on day one and Start run can happen the moment the threshold is hit, with no campaign deadline.
Campaign card · four pot states
Checkout · what the money is
- Held for this loop and spent only as model tokens on this run
- Fees: [unset · do not invent a % here]
- Creator receives $0 cash. Start run boots a machine, not a payout.
- Unused money refunds pro-rata when the creator ships or the loop is cancelled
Creator funding panel · three numbers
Money flow · pledge to token
Key limit is a hard backstop, not the primary brake: loopd refuses to start a wave when limit_remaining < reserve, so most pauses land on a clean wave boundary. Reserve ≈ max($10, 3% of pot) covers in-flight Fable output on parallel subagents.
Unit economics before any take rate
| Per $100 pledged | Cost | Note |
|---|---|---|
| Stripe processing | ≈ $3.20 | 2.9% + $0.30. Not returned on refunds. |
| OpenRouter credit-purchase fee | ≈ $5.30 | 5.5% on credits Loopfund buys in bulk; platform carries the float; credits expire after 365 days. |
| Sandbox compute | ≈ $6.50 / run-day | 4 vCPU / 8 GB on E2B or Daytona; $0 while paused (storage only). |
| Gone before Loopfund earns anything | ≈ 8.5% + $6.50/day | Take rate is an open question; 0% is a loss. |
03 · Campaign + run state machine
One run per campaign in MVP. Three resources are bound to the run and must move together: the VM (disk + memory), the OpenRouter key, and the Claude Code session.
| State | VM | OpenRouter key | Claude session | Backers see |
|---|---|---|---|---|
| FUNDING / FUNDED | — | — | — | pot meter, threshold, credentials |
| PROVISIONING | create from template, clone repo, write bar/, .claude/ | POST /keys limit = pot − reserve | none | "Booting" (mock 08), spend $0 |
| RUNNING | running | enabled | active; --resume across waves | workbench, 11-shot, verdicts, spend |
| PAUSED (pot) | pause() with memory | disabled: true | frozen in RAM | "Paused · add fuel to resume" |
| PAUSED (provider) | pause() | enabled | frozen | "Provider outage · retrying" |
| PAUSED (creator / plateau) | pause() at wave boundary | enabled | frozen | "Paused by creator" / vote widget |
| SHIPPED | final capture + build → CDN; kill after 7 d | disabled → deleted | archived to storage | playable, final episode, refund notice |
04 · Running the Gauntlet Loop through OpenRouter
OpenRouter replaces the pipe, not the harness. Claude Code speaks its native Anthropic protocol to https://openrouter.ai/api with the per-run key as bearer token. Provider pinning, failover and model choice live in OpenRouter Presets referenced by name from Claude Code's model-slot env vars, so no proxy is needed for MVP.
Bearer sk-or-… (limit = pot − reserve)"| ORK[OpenRouter key check
limit_remaining > 0 ?] ORK -->|"model = @preset/loopfund-fable"| PRE[Preset: pinned model
provider.order anthropic → bedrock → vertex
only those 3 · allow_fallbacks true] PRE --> A1[Anthropic 1P] PRE -. 429 / 5xx .-> A2[Amazon Bedrock] PRE -. 429 / 5xx .-> A3[Google Vertex] ORK -->|"402 when limit hit"| CC ORC[Orchestrator] -->|"POST/PATCH /keys · GET usage"| ORK ORC -->|edit presets for routing experiments| PRE
Env in the VM (from the OpenRouter Claude Code cookbook)
ANTHROPIC_BASE_URL=https://openrouter.ai/api
ANTHROPIC_AUTH_TOKEN=<per-run OpenRouter key> # minted via Management API, limit = pot − reserve
ANTHROPIC_API_KEY= # must be explicitly empty
ANTHROPIC_DEFAULT_FABLE_MODEL=@preset/loopfund-fable
ANTHROPIC_DEFAULT_OPUS_MODEL=@preset/loopfund-opus
ANTHROPIC_DEFAULT_SONNET_MODEL=@preset/loopfund-sonnet
ANTHROPIC_DEFAULT_HAIKU_MODEL=@preset/loopfund-haiku
CLAUDE_CODE_SUBAGENT_MODEL=@preset/loopfund-fable # default for subagents the lead spawns
CLAUDE_CODE_ENABLE_TELEMETRY=1
OTEL_METRICS_EXPORTER=otlp OTEL_LOGS_EXPORTER=otlp OTEL_EXPORTER_OTLP_ENDPOINT=<orchestrator collector>
Per-role routing table
| Role | How Claude Code picks it | Preset (MVP) | Pinned model | Provider order | May be downgraded in experiments? |
|---|---|---|---|---|---|
| Lead / overseer | session model (Fable slot) | @preset/loopfund-fable | anthropic/claude-fable-5.1 (never a -latest alias; price must be predictable) | anthropic → amazon-bedrock → google-vertex; only those three | No |
| Builder subagents | .claude/agents/builder.md model: fable or opus | @preset/loopfund-fable / -opus | Fable 5.1 / Opus 5 | same | Yes — this is where "optimize routing" happens (Opus or Sonnet builders on cheaper campaigns) |
| Critic subagents | .claude/agents/critic.md model: fable | @preset/loopfund-critic | Fable 5.1 | same | Never (spec rule 5) |
| Smoother (optional) | smoother.md | @preset/loopfund-opus | Opus 5 | same | Yes |
| Auxiliary (titles, compaction) | Haiku slot | @preset/loopfund-haiku | Haiku | same | Yes |
Changing a preset changes routing for every run pointing at it without touching the harness. For cohort experiments, mint per-cohort presets (@preset/loopfund-opus-expA) and set the env at provisioning.
The wave loop · what loopd does
pot, paused?, nudges, session_id] G --> C{paused or
limit_remaining < reserve?} C -- yes --> P[request pause → orchestrator pauses VM] --> S C -- no --> N[append nudges → NUDGES.md] N --> W["run one wave:
claude -p --resume $SESSION --output-format stream-json <wave prompt>"] W --> R{exit} R -- ok --> T[parse transcript → api_calls per agent/model
read WAVE.json] T --> PUB[publish: workbench + 11-shot + build → CDN
git push · GitHub Release wave-N] PUB --> RP[report wave → orchestrator] --> PL{plateau: N flat waves?} PL -- yes --> P PL -- no --> G R -- api_error 429/529 --> BO[backoff 60 s → 10 min, ≤ 6] --> W BO -- cap hit --> P R -- 402 limit --> P
Why an external supervisor: Claude Code force-ends a turn after 8 consecutive Stop-hook blocks and expects hooks to yield when stop_hook_active is set. A Stop hook can guard one turn ("you did not write WAVE.json, keep going"); the loop across turns must be started from outside. That is also what gives us clean boundaries for pause, nudges and publishing.
The wave prompt (kept in Matt's register)
Loopfund wave protocol. You are running inside a hosted Gauntlet Loop.
- The goal and the bar are above / in bar/. Do not change the bar.
- Split the work however you judge best. Every important piece gets a builder and a separate,
fresh-context critic that inspects the real artifact and compares it blind against bar/.
- Read NUDGES.md at the start of each wave. Treat entries as notes for the critics, not as
architecture instructions. Never wait for input.
- Keep workbench/index.html current (shots, scores, what changed, what is next).
- At the end of each wave write WAVE.json: pieces, per-piece critic verdict and score,
biggest remaining gap, next step. Commit. Then keep going; there is no final round.
- Method reference is in /opt/loopfund/reference/. Smoothing pass at wave end is your call.
Provider 429 and empty pot · what the ledger records
05 · The sandbox: what is in the box
A Firecracker microVM (E2B or Daytona; Fly Sprites as the fallback) built from one template. The creator never logs into it. It holds one OpenRouter key scoped to the pot, a short-lived GitHub App token scoped to one repo, and a run token for the orchestrator API. Pause keeps memory and disk; resume brings back running processes.
| Concern | Decision |
|---|---|
| Image | Ubuntu 24.04, Node 22, pnpm, git, Python, ffmpeg, ImageMagick, Chromium, Claude Code (pinned), Agent SDK, loopd, tools, reference docs. |
| WebGL without GPU | SwiftShader: seconds per frame, fine for stills and 11-shot capture; not for frame-time profiling. GPU tier (Modal/Daytona/Beam) later, not MVP. |
| Egress | openrouter.ai, registry.npmjs.org, github.com/api.github.com, orchestrator API + OTel. Nothing else. No inbound. |
| Pause / resume | Sandbox pause() with memory (≈4 s/GB) → processes and RAM survive. Fallback: kill + claude --resume <session> on the same disk. E2B 24 h continuous clock → loopd schedules a pause/resume at the first wave boundary after 20 h. |
| Durability | Repo pushed to GitHub every wave; transcripts mirrored to R2 every wave. Host loss = new VM, clone, restore transcript, resume. Loses ≤ 1 wave. |
| Critic enforcement | PreToolUse hook denies Edit/Write/MultiEdit when agent_type ~ /critic/i. Default critic.md has tools Read/Glob/Grep/Bash only. |
| Serving the workbench | Static snapshot to CDN every 5 min and at wave end. Never a tunnel to :5173 (exposes the VM; dies on pause). |
| Break-glass | Creator PTY attach via the sandbox provider, logged as a ledger event, visible to creator + ops only. Rotate the OpenRouter key on detach. |
13 · hosted sandbox vs your browser
11 · today: Claude Code on your laptop
12 · today: 11-shot critic capture
06 · Creator tools: console wireframes
Creator control is a capture/review surface plus a brake. It is not a weaker Claude Code; it is Claude Code the creator is not allowed to babysit. Four verbs: watch, playtest, nudge (queued for the next critic round), pause / ship. Attach is break-glass.
Sandbox · hosted by Loopfund · you are not in the machine
- 007 Lighting WON mood and readability consistent
- 006 Trees LOST pop-in and clustering at mid distance
- 005 Gun WON recoil and muzzle feedback
- 004 Lighting WON god rays support focal read
- 003 Trees LOST foliage density
from transcript parsing, reconciled to OpenRouter key usage
Queued · lands at wave 8 start view queue (2)
Nudges are notes for the critics, not orders to the builder. They never interrupt a live wave.
Elaborates mocks 10 and 14. New vs the mocks: per-role spend, "pause at wave end" vs "pause now", queued-nudge ETA tied to the wave boundary, break-glass rotates the key on detach.
from SubagentStart/Stop hook events, not ps
Wizard · what the creator supplies (and what they do not)
| Step | Creator supplies | Platform does | Deliberately absent |
|---|---|---|---|
| 04 Goal | Destination in a paragraph, category, model class (Fable 5.1 default) | Stores verbatim as the top of the run prompt | Feature list, architecture, task breakdown |
| 05 Bar | Reference screenshots / playable URL / test suite / reference repo; homage checkbox with IP warning | Stores at bar/, read-only; if empty, first wave = find a bar | Text-only "make it AAA" |
| Credentials | GitHub OAuth (required), prior projects, jobs, press, testimonials | Renders the sidebar (mock 15); marks linked vs verified | — |
| Threshold | Start threshold (default $500), optional goal | Enables Start run at threshold; refunds if never started | Reward tiers, deadline pressure |
| 07 Start run | One click after reading "this is not a payout, cash-out $0" | Mint key, boot VM, inject goal + bar, first wave | Any withdrawal path |
07 · Watch, episodes, streaming
The stream is the workbench, not a terminal. The agent writes it; the platform snapshots and serves it. Episodes are GitHub Releases, one per critic wave. Twitch is an optional ingest of the same composite, later.
lead-maintained] SH[tools/shotset → shots/wave-N/*.png
+ one shot every 5 min] BUILD[vite build → dist/] WJ[WAVE.json] LD[loopd publisher] WB & SH & BUILD & WJ --> LD end LD -->|every 5 min + wave end| R2[(R2 / CDN)] R2 --> W1["/watch/slug · live workbench, 11-shot, spend, verdicts"] R2 --> W2["/watch/slug/wave/N · episode page"] R2 --> PLAY["/play/slug · playable (signed URL for backers until ship)"] LD -->|"git push · gh release create wave-N"| GH["GitHub Release wave-N
score table · biggest gap · next step
assets: grid.png, shots, playable.zip, WAVE.json"] R2 -.->|"Phase 3: 5-min shots → ffmpeg → 60 s speedrun.mp4"| VOD["2×/5× VOD on /watch"] LD -.->|"Phase 3 optional: ffmpeg composite → RTMP"| TW[Twitch ingest
workbench + playable + critic overlay]
2×/5× = speedrun VOD stitched from 5-minute shots (Phase 3)
Add fuel
next_step_options · closes at wave end · result becomes one line in NUDGES.mdElaborates mock 16. Chat is votes between waves only; no free-text from backers reaches the harness in MVP (prompt-injection surface, and rule 3).
| Surface | Source | Cadence | Phase |
|---|---|---|---|
| Live workbench | workbench/index.html snapshot | 5 min + wave end | MVP |
| 11-shot grid + verdicts | shots/wave-N, WAVE.json | wave end | MVP |
| Episode (GitHub Release) | loopd via GitHub App | wave end | MVP |
| Playable for backers | Vite build → CDN, signed URL | wave end | MVP |
| 2×/5× speedrun VOD | 5-min shots → ffmpeg | hourly | Phase 3 (start archiving shots in MVP so it is cheap later) |
| Twitch ingest | ffmpeg composite in VM → RTMP | continuous while running | Phase 3, optional |
08 · Observability and routing optimization
Three sources, reconciled per wave. Financial truth is the key. Attribution truth is the transcript. Live cross-check is OTel.
| Source | Gives | Does not give | Used for |
|---|---|---|---|
OpenRouter key usage / limit_remaining (poll) | Exact USD debited per run | Per call, per agent | Pot, pause decisions, refunds |
Claude Code session transcript JSONL (+ subagent transcripts), parsed by loopd | Per API turn: model, input/output/cache tokens; joined to SubagentStart/Stop hook events → agent_id, agent_type | Exact price (estimated from list) | Builder vs critic split, per-piece cost, cache-hit ratio |
| Claude Code OTel metrics/events | token.usage, cost.usage by model, type, query_source, effort; api_request events | Custom agent names (redacted to "custom") | Live dashboards, alerts (cache-hit collapse, spend spikes) |
Phase 2 proxy: GET /api/v1/generation?id= | Exact native cost + serving provider per call | — | Replaces estimates; per-call reserve/commit; per-run provider prefs for experiments |
The metric to optimize
Critic-score delta per dollar per wave, split by builder model. Stored from WAVE.json as a series so it is queryable without parsing HTML. Secondary: cache-hit ratio (first efficiency signal; loops reread the growing repo), 429 count and provider mix, wall time per wave, verdict win rate. Experiments change presets for a cohort of runs; critics are never in the experiment.
| Signal | Backer | Creator | Ops |
|---|---|---|---|
| Workbench, grid, playable, verdicts, pot totals | ✓ | ✓ | ✓ |
| Per-role spend, processes, nudge queue, provider handoffs | — | ✓ | ✓ |
| Per-agent api_calls, transcripts, break-glass log, full ledger | never | own break-glass only | ✓ audited |
09 · Phases
Build the machine before the marketplace. The spec's success bar is entirely about the run: one hosted loop survives a provider 429 and an empty-pot pause/resume, with a public workbench and a GitHub Release episode, funded by a real Stripe test payment.
Prove the loop survives hosting
- Docker image: Claude Code, Chromium, tools,
loopd, hooks, subagent defaults - One OpenRouter key,
limit: 25, presets created - Run Matt's
prompt.mdagainstbar/of CoD frames - Transcript parser ↔ key usage within 5%
- Gate: preservation test passes (critic denied on Edit, wave 2 starts unattended, stops only at $25)
Sandbox, brakes, money, one public run
- E2B/Daytona template; orchestrator state machine; key mint/patch/disable
- Chaos: 429 for 10 min, limit→$0 mid-wave, kill VM → recover
- Stripe charge-now pledges, threshold → Start run, top-ups, pro-rata refunds, ledger + reconciliation
/watchfrom R2 snapshots; GitHub Release per wave; playable via signed URL- Creator console: nudge, pause (wave end / now), ship, break-glass (logged)
- Real campaign + checkout pages; wizard 04/05/credentials; example campaigns labeled
Make it measurable and safer
- Pass-through gateway proxy: exact per-generation cost, per-call reserve/commit (mock 17 for real), per-run provider prefs
- Routing experiments: builder-model cohorts, Δscore/$ dashboard
- Plateau checkpoint with backer vote; between-wave votes from
next_step_options - Stripe Identity gate above a pot threshold; manual review queue
- Ops runbook; alerts on idle, spend spikes, cache-hit collapse
Distribution and new verticals
- 2×/5× speedrun VODs from archived 5-min shots; 60 s clip attached to each release
- Optional Twitch ingest (workbench + playable + critic overlay)
- GPU sandbox tier for real WebGL performance
- Open-source-on-ship flow; hosted playable at
/play/slug - Second gateway (Anthropic Admin API workspaces) for cost; Codex harness option
- Interactive tools, then movies/episodic; research only with a public eval
MVP in / out (unchanged from SPEC.md)
In. Games only; creator submit (destination, Fable 5.1 via OpenRouter, critic bar, credentials); Stripe Checkout into the pot; no creator withdrawal; Start run boots a scoped VM and mints an OpenRouter key with limit = pot − reserve; public workbench; 11-shot critic grid; empty-pot pause / top-up resume same disk; pro-rata unused refund; GitHub Release each wave; creator watch / nudge / pause / ship; break-glass logged and hidden from backers.
Out. Creator cash-out; equity; chat interrupting the builder; pooling Max subs; movies/research; full 2×/5× pipeline; multi-gateway beyond OpenRouter; custom domains; GPU sandboxes; per-call reserve/commit ledger (Phase 2).
Decided 2 Sep 2026
| Question | Decision |
|---|---|
| Sandbox provider | E2B first; Daytona as tested fallback. Re-check after Phase 1 chaos tests. |
| Pledge model | Charge now; full refund if the run never starts. Loopfund eats the Stripe fee on those. |
| Take rate | 15% all-in. "85% becomes tokens." Shown before payment. |
| Critic model | Always Fable 5.1. Builders may be downgraded in experiments; critics never. |
| Per-call ledger (mock 17) | Do it right: pass-through proxy comes right after Phase 0 passes, not in Phase 2. |
| "Escrow" | Removed. "Held for this loop." |
| Session strategy | One long resumed session per run; fresh session only on corruption. |
| Backer votes | Not in MVP. Creator-only nudges. |
| Plateau | 5 flat waves → pause and ask the creator; advisory; auto-cancel + refund after 14 days of silence. |
| Reference-bar IP | Private in bar/, never published. |
Still open
- Legal: stored-value / gift-card review of "prepaid compute, refundable if unused, creator gets $0". Not blocking the prototype.
- Exact OpenRouter model slugs for the presets (confirm on the models page before creating them).


