# Loopfund — implementation gap analysis

Companion to `SPEC.md` (2 Sep 2026). This document does not change any load-bearing rule in the spec. Where it proposes an answer to an **Open question**, the answer is labeled *Proposed* and must still be confirmed by Miguel before it appears in product copy. Where a fact about a third-party system is load-bearing, the source is named so it can be re-verified at implementation time.

Sections:

0. Reconciliation with the Grok spec-bot thread (2 Sep 2026)
1. The Gauntlet Loop invariants and how the platform preserves each one
2. Layer boundaries (overseer / harness / platform)
3. Reference architecture
4. Run state machine
5. Money: ledger, Stripe, OpenRouter key, reserve, unit economics
6. Sandbox specification
7. Harness wiring inside the sandbox (the part that makes it a real Gauntlet Loop)
8. Observability model and data model
9. Nudges, votes, break-glass
10. Failure handling: 429, empty pot, crashes, compaction
11. Episodes and distribution
12. Web app surfaces and routes
13. Proposed answers to the spec's open questions
14. Gaps the spec does not mention
15. Build order and test matrix
16. Questions for Miguel

---

## Decisions (Miguel, 2 Sep 2026 16:57)

These answer §16 and override any *Proposed* label below. Where Miguel delegated ("you decide", "whatever is most likely to work"), the choice and the reason are recorded so it can be reversed deliberately.

| # | Question | Decision | Notes |
|---|---|---|---|
| 1 | Sandbox provider | **E2B first**, Daytona as the tested fallback | "Most likely to work": E2B has the most-used Claude Code / Agent SDK templates, memory-preserving `pause()`/`resume`, and an SDK the orchestrator can drive from Node. The 24 h clock is handled by `loopd` (§10). Re-evaluate on Phase 1 chaos-test results. |
| 2 | Pledge model | **Charge now**, refund if never started | Miguel accepts eating Stripe fees (~3%) on campaigns that never start. |
| 3 | Take rate | **15% all-in** | Copy: "15% covers payment processing, gateway, and hosting. 85% becomes tokens." Shown before payment. |
| 4 | Critic model floor | **Critic is always Fable 5.1** | Builders may be routed to cheaper models in experiments; critics never. Preset `@preset/loopfund-critic` is pinned and not part of any cohort. |
| 5 | Mock 17 per-call ledger | **Do it right: pass-through proxy in MVP** | Recommendation adopted. A thin Anthropic-protocol pass-through in front of OpenRouter gives per-call `reserved → settled` rows, exact cost via `GET /generation?id=`, and per-run provider prefs, at the cost of one more service on the request path. Kept optional via env: `LOOPFUND_GATEWAY_URL` unset → Claude Code talks to OpenRouter directly (Phase 0), set → proxy (Phase 1+). |
| 6 | "Escrow" wording | **Remove now.** "Held for this loop." | Applied across site copy in this commit. |
| 7 | Session strategy | **One long resumed session per run** (`--resume`), fresh session only on corruption | "Whatever most likely yields Gauntlet Loops": Matt's runs are single long sessions with auto-compaction; the lead's accumulated judgment about the bar is part of what makes late waves good. `WAVE.json` + repo remain the durable memory so a forced fresh session can recover. |
| 8 | Backer votes | **Creator-only nudges in MVP** | No backer free text or votes reach the harness. Votes return in Phase 2 once `next_step_options` behavior is observed. |
| 9 | Plateau checkpoint | **N = 5 flat waves → auto-pause, notify creator; advisory only; auto-cancel + pro-rata refund after 14 days of creator silence** | Delegated. Never binding on the creator while they are responsive; never auto-kill an improving run. |
| 10 | Reference-bar IP | **Yes**: copyrighted references stored privately in `bar/`, never published | Grid on `/watch` shows our frames only; creator console may show bar thumbnails. |

**Priorities (Miguel):** (1) working prototype of the backer side: browse, campaign, pay/donate for real; (2) creator side: post a project with a video; (3) the sandbox running a real Gauntlet Loop through OpenRouter end to end. Build order in §15 is re-sequenced accordingly: Phase 0 harness and Phase 2 money now run in parallel; public surfaces get high-fidelity HTML before any backend.

---

## 0. Reconciliation with the Grok spec-bot thread

Source: `~/Downloads/loopfund-thread.md` (export of the Grok "Spec bot" conversation that produced `SPEC.md`). Where the thread and `SPEC.md` disagree, `SPEC.md` is later and wins; conflicts are listed so they are decided, not silently dropped.

**Thread decisions this document adopts (were not explicit in `SPEC.md`):**

| Thread decision | Where it lands here |
|---|---|
| Stripe Checkout **captured immediately** into the pot "so the run can start at a low threshold instead of waiting 30 days" (t5). | §5.3 default flipped to charge-now. Authorize/capture kept as the alternative. |
| Auth: creator = GitHub OAuth; backer = guest + email at checkout, GitHub optional; sandbox has no Anthropic login. | §12. |
| "If the critic stops finding real gaps for N rounds, auto-pause and ask backers whether to keep going." | §10 plateau checkpoint. A human still decides, so I5 holds. |
| In-flight buffer of "a few dollars" so a request in the air does not bounce at zero, then pause. | §5.2 reserve. |
| Creator may put their own money in as fuel. | §5.3 top-ups (anyone, including the creator). |
| Pot sizing: $500–$1,500 is a full game-scale loop; $500 example start threshold. | §5.5 defaults for example campaigns (labeled *Example project*, per the spec). |
| Sandbox candidates: E2B, Daytona, Fly. Matt himself isolates runs in a Daytona sandbox after an agent wiped his Mac. | §3 table; Daytona added as a credible alternative. |
| Chat votes between waves only; do not let chat interrupt the builder; Twitch streams workbench + playable + critic overlay, never a raw terminal. | §9, §11 (already aligned). |
| Category stack-rank and closest-neighbor research (t33). | Appendix A. |

**Conflicts to decide:**

1. **Per-call reserve/commit ledger.** The thread's gateway (t9) and mock 17 show every model call as a "Call reserved → Call settled" ledger row with the gateway choosing the upstream. With OpenRouter direct (spec rule 5, thread t12), Loopfund does not sit on the request path, so MVP cannot produce per-call *reserved* rows; it gets per-key usage (polled) plus per-call detail after the fact from the transcript (§8.2). Per-call reserve/commit exists only if the Phase 2 pass-through proxy (§8.4) is built. Mock 17 should be read as the Phase 2 target, or the ops ledger UI should show "settled" rows only for MVP.
2. **Open-source default.** Early thread (t2): "Open-source artifact by default, because a revenue-share Kickstarter is a securities problem and a patronage Kickstarter is a product." `SPEC.md` Q9 lean: default off. Spec wins here, but the thread's reasoning is worth keeping: if backers cannot get the artifact, "early access + playable" has to be delivered another way (§11 signed-URL assets).
3. **"Escrow" wording.** The thread coined "escrow" as the product word for "prepaid compute wallet" and explicitly said "Legally you do not run a bank." The live site uses "escrow" in ~12 places (`backer.html`, `checkout.html`, `project.html`, `index.html`, `creators.html`, `spec.html`, `mvp.html`, meta descriptions). §13 Q1 recommends replacing it with "held for this loop" in consumer copy before counsel review, because "escrow" has a legal meaning that implies a third-party fiduciary, which Loopfund is not.
4. **A "100% goes to the creator" line leaked into one generated mock** (t8, spec bot said to ignore it). Not found in current HTML copy; verify mock PNGs 02/15 before they are used as full-bleed placeholders.
5. **Multi-upstream gateway with Codex as Upstream C** (t9 diagram). Out of MVP per spec ("multi-gateway beyond OpenRouter" is out). Kept as v2 in §8.4.

**Thread items that are already superseded:** the Fable 5.1 cloud spec-review (`bc-609f…`) was stopped before producing `SPEC-REVIEW.md`; this document is that review. PR #1 / claim-deployment URLs: `~/loopfunder` on the linked Vercel project is production (spec Q10).

---

## 1. Gauntlet Loop invariants

Derived from the canonical post (somethingbig.ai/gauntlet-loop, 27 Jul 2026), the Claude-of-Duty `prompt.md`, and the CoD `README.md` process notes. These are the things that, if the platform breaks them, mean we are hosting "an agent that writes games" and not a Gauntlet Loop.

| # | Invariant | Source | Platform mechanism that preserves it | What would break it (do not do) |
|---|---|---|---|---|
| I1 | **Goal, not implementation.** The lead agent decides the decomposition. | "Give it the destination. Let it choose the route." | The run prompt is the creator's goal + bar + a short Loopfund wave-protocol addendum. No workstream list, no architecture, no subsystem plan injected by the platform. | Creator wizard fields that become a task list; platform-generated "plan" step; nudges that read as architecture orders. |
| I2 | **A real bar the critic can inspect.** | "Make it amazing is not a bar." | Step 05 (create-bar) requires uploadable reference artifacts (screenshots, a playable URL, a test suite, a reference repo) stored at `bar/` in the run repo, read-only to the agents. If the creator has none, the wizard offers the "find a bar" prompt from the post as the first wave's task. | Text-only bars; letting the builder redefine the bar mid-run. |
| I3 | **Lead splits the work into the smallest independently judgeable pieces.** | Section 3 of the post. | Nothing. The platform must not pre-split. The wave protocol only asks the lead to *report* the pieces it chose (in `WAVE.json`) so the console can show them. | UI that lets backers/creators add "workstreams". |
| I4 | **Builder never grades itself. Critic is a fresh-context subagent that inspects the actual artifact, blind A/B against the bar, names the biggest gap.** | Section 4. | (a) Claude Code subagents are fresh-context by construction. (b) Default `critic` subagent definition is shipped with tools = Read, Bash(capture/playtest), no Edit/Write. (c) A `PreToolUse` hook denies Edit/Write/MultiEdit when `agent_type` matches `critic*`, so even a lead-invented critic cannot fix what it grades. (d) Capture tools produce reproducible frames (`baseline.mjs` pattern) so the critic sees pixels, not prose. | Routing critics to a cheaper model (spec rule 5 already forbids). Letting the critic see builder transcripts. Grading a builder-written summary. |
| I5 | **Unbounded loop. No fixed round count. Only a human (or the pot) stops it.** | Section 5. | An external **wave supervisor** (`loopd`, §7.1) starts the next turn after each wave until `pot_remaining < reserve`, creator Pause/Ship, or platform incident. The stop condition is money and humans, never a counter. | Using Claude Code `Stop`-hook blocking as the loop primitive: Claude Code force-ends a turn after 8 consecutive Stop-hook blocks (`CLAUDE_CODE_STOP_HOOK_BLOCK_CAP`, issue #85547) and hooks must yield when `stop_hook_active` is true. A hook-driven loop dies at round 8. |
| I6 | **Real harness.** Files, shell, screenshots, dev server, real subagents. | "Do not paste this into a normal Claude chat." | Claude Code (headless / Agent SDK) inside a microVM with Node, headless Chromium (WebGL via ANGLE/SwiftShader), Vite, git, and the CoD-style `tools/` capture scripts pre-installed. | A chat-completions loop that fakes tools; a "lite" harness. |
| I7 | **Watch without interrupting.** Live workbench the agent maintains. | "Watch it without interrupting it." | The lead is asked to maintain `workbench/index.html`; the platform snapshots it to a CDN every wave (and every few minutes) and serves it read-only at `/watch/<slug>`. Nudges are queued to a file the lead reads only at wave start. | Live TTY to backers; chat piped into the running session. |
| I8 | **Optional smoothing pass at wave end.** | "An optional smoothing pass." | Mentioned once in the wave-protocol addendum as optional. Not enforced. | Forcing it every wave. |
| I9 | **Process learnings from CoD are reference, not rules.** Sequential single-owner passes beat parallel fan-out on coupled systems; `ARCHITECTURE.md` as the inter-agent contract; reproducible captures; per-pixel diff gate. | CoD README "Process note", "Tooling". | Ship the CoD README process note and tooling patterns as reference material in the sandbox image (`/opt/loopfund/reference/`). The lead can read them; the platform does not mandate them. | Encoding "max N parallel subagents" into the platform. |

**Preservation test (run before every release):** take Matt's exact `prompt.md`, point it at a $25-capped run, and confirm that (1) the lead spawned at least one builder and one separate critic, (2) a critic produced a blind A/B verdict against `bar/`, (3) the run continued into a second wave without a human, (4) it stopped only when the key hit its limit. If any of those fail, the platform has broken an invariant.

---

## 2. Layer boundaries

The spec says "do not mix the layers". Three layers, one new:

- **Overseer** = the prompt. Owned by the creator (goal + bar). Loopfund appends a *wave-protocol addendum* (≈12 lines) that only covers how to report progress and where to read nudges. Nothing about how to build.
- **Harness** = Claude Code + subagents + tools. Unmodified. Same binary the creator would run locally. Loopfund adds hooks, subagent defaults, env, and telemetry config via `.claude/settings.json` in the run repo.
- **Platform** = the room the machine runs in. Money (Stripe → pot → OpenRouter key), the VM, the wave supervisor, observability, publishing. The platform is allowed in the loop's decision path at exactly two points: the **budget brake** (key limit / pause) and the **wave boundary** (nudges appended to `NUDGES.md`, next-wave prompt). Nowhere else.

Anything that wants to influence the build mid-wave is out of scope by rule 3 and I7.

---

## 3. Reference architecture

```
                 ┌──────────────────────────────────────────────────────────────┐
                 │  Web app  (Next.js on Vercel, existing prj_u0s9…)             │
   Backer ─────▶ │  /  campaign  checkout  watch/<slug>  creators/*  ops/*        │ ◀──── Creator
                 └───────────────┬──────────────────────────────┬───────────────┘
                                 │ Stripe webhooks               │ REST/Realtime
                        ┌────────▼────────┐             ┌───────▼────────────────┐
                        │ Stripe          │             │ Postgres (Supabase)     │
                        │ PaymentIntents  │             │ campaigns pledges runs  │
                        │ manual capture  │             │ waves agents api_calls  │
                        └────────┬────────┘             │ shots verdicts ledger   │
                                 │                      │ episodes events         │
                                 ▼                      └───────▲────────────────┘
                 ┌──────────────────────────────────────────────┴───────────────┐
                 │  Run Orchestrator  (long-lived worker: Fly Machine or Railway) │
                 │  state machine · ledger writer · OpenRouter Mgmt API client    │
                 │  sandbox lifecycle · OTel collector · GitHub App · publisher   │
                 └───────┬───────────────────────┬────────────────────┬─────────┘
                         │ create/pause/resume    │ mint/patch/disable  │ push release
                ┌────────▼─────────┐    ┌────────▼─────────┐  ┌───────▼────────┐
                │ Sandbox (E2B     │    │ OpenRouter        │  │ GitHub org     │
                │ Firecracker VM)  │───▶│ per-run key       │  │ loopfund-runs/ │
                │  loopd           │    │ limit = pot-res.  │  │ <slug>         │
                │  claude (lead)   │    │ presets: lead,    │  │ release/wave-N │
                │   ├ builder subs │    │  critic (Anthropic│  └────────────────┘
                │   └ critic subs  │    │  1P first)        │
                │  chromium, vite  │    └───────────────────┘
                │  tools/capture   │
                │  workbench/      │──── static snapshot ────▶ CDN (R2) → /watch/<slug>
                └──────────────────┘
```

Component choices and why:

| Component | Choice for MVP | Why | Alternatives considered |
|---|---|---|---|
| Web app | Next.js on the existing Vercel project | Already the production domain. Serverless is fine for pages/webhooks. | — |
| DB | Supabase Postgres + Realtime | Boring ledger table (spec), realtime for the console. | Neon + Pusher |
| Payments | Stripe Checkout, **charged immediately** into the pot (thread decision t5); refund if the campaign never reaches Start run | Pot is real money from the first pledge, so Start run can happen the moment the threshold is met, with no campaign deadline and no capture shrinkage. Top-ups are the same flow. Cost: Stripe keeps its processing fee on refunds, so an unfunded campaign costs Loopfund ~3% of pledges. | Authorize-now/capture-at-threshold (`capture_method=manual` + extended auth, ≤30-day campaigns, no fee loss, but shrinkage at capture and a hard deadline). Stripe Connect (not needed: creator gets $0). |
| Orchestrator | One long-lived Node service (Fly Machine, 1 GB) | Needs a process that outlives a Vercel function: sandbox lifecycle, OTel receiver, polling OpenRouter key usage, retries. | Inngest/Temporal (fine later). Vercel Workflow (5h fn cap is awkward for OTel receiver). |
| Sandbox | **E2B** sandboxes (Firecracker) with `pause()` (filesystem + memory) | Pause preserves running processes and RAM; paused sandboxes are kept indefinitely with no TTL; resume ≈1 s; billing stops on pause. This is exactly "sleep the VM, resume the same disk". 24 h max continuous runtime on Pro; the clock resets on resume, and `loopd` can schedule a pause/resume at a wave boundary before 24 h. | **Daytona** (pause/archive persistence, GPU available, no subscription floor; Matt runs his own loops in a Daytona sandbox, which is a credibility point for creators). Fly Sprites (no duration cap, auto-sleep, 100 GB durable storage; checkpoint/restore). Fly Machines (suspend limited to ≤2 GB RAM; use stop/start + volume). Modal (GPU). Vercel Sandbox (5 h cap: no). Decide E2B vs Daytona in Phase 1 on measured pause fidelity and price. |
| Model gateway | OpenRouter, one key per run via Management API, model refs via **Presets** | Key `limit` (USD) is a hard cap; `disabled: true` is an instant kill; `usage`/`limit_remaining` readable per key. Presets carry `provider.order/only/allow_fallbacks` server-side so Claude Code's requests get Anthropic-1P pinning without a proxy. | Thin Loopfund proxy in front of OpenRouter (Phase 2, §8.4). Anthropic Admin API workspaces with spend limits (v2 "multi-gateway", out of MVP). |
| Telemetry | Claude Code OTel (`CLAUDE_CODE_ENABLE_TELEMETRY=1`) → OTel collector in the orchestrator; hooks → JSONL; **session transcript JSONL parsed by `loopd`** | Three sources cross-check each other (§8.2). | LiteLLM proxy for per-key spend (redundant with OpenRouter key usage). |
| Publishing | GitHub App installed on a platform-owned org; repo per run; release per wave | Spec: Releases are the episode format. | — |
| Workbench/playable hosting | Static snapshot pushed to Cloudflare R2 (or Vercel Blob) and served under loopfunder.com | Read-only, no live port on the VM exposed, survives pause. | Tunnel to VM port 5173 (exposes the VM, dies on pause). |

---

## 4. Run state machine

One `run` per campaign in MVP. States and what happens to the three bound resources (VM, key, session):

| State | Entered when | VM | OpenRouter key | Claude session | Ledger |
|---|---|---|---|---|---|
| `DRAFT` | Creator finishes wizard | — | — | — | — |
| `FUNDING` | Creator publishes | — | — | — | pledges authorized (uncaptured) |
| `FUNDED` | `sum(authorized) ≥ threshold` before deadline | — | — | — | capture all; `pot_credit` rows |
| `PROVISIONING` | Creator clicks **Start run** | create from template; clone run repo; write `bar/`, `.claude/`, env | `POST /keys` `limit = pot_available − reserve`, `expires_at = null` | none | `key_minted` |
| `RUNNING` | `loopd` heartbeat | running | enabled | active (`--resume` across waves) | `spend_settled` rows every poll |
| `PAUSED(pot_empty)` | key `limit_remaining ≤ 0` or 402 from gateway | `pause()` (memory kept) | `disabled: true` | frozen in memory | `pause` |
| `PAUSED(provider)` | N consecutive `StopFailure` with 429/529 after gateway failover | `pause()` | enabled | frozen | `pause` |
| `PAUSED(creator)` | creator clicks Pause | pause at next wave boundary (default) or immediately (flag) | enabled | frozen | `pause` |
| `PAUSED(platform)` | ops action / incident | `pause()` | `disabled: true` | frozen | `pause` |
| `RUNNING` (resume) | top-up (`PATCH limit`), provider recovery, creator Resume | `resume()` | `disabled: false`, `limit` raised | continues; in-flight HTTP retried by Claude Code | `resume`, `topup` |
| `SHIPPED` | creator clicks Ship (or lead reports bar met and creator confirms) | final capture + build → CDN; then `kill()` after 7-day grace | `disabled: true` | archived (transcript mirrored to storage) | `ship`, `refund` (pro-rata unspent) |
| `CANCELLED` | deadline missed / creator cancel / abandonment rule (§13 Q4, Q6) | `kill()` if provisioned | `disabled: true`, then delete | archived | void authorizations or refund captured |
| `SETTLED` | all refunds succeeded | — | deleted | — | closed |

Invariants: a run can be `PAUSED` for any reason for any duration (E2B has no TTL on paused sandboxes; we add our own: 30 days paused → notify → 60 days → `CANCELLED` with refund, *Proposed*). Top-ups after `Start run` are immediate captures, not authorizations.

---

## 5. Money

### 5.1 Ledger (the boring table)

```
ledger_entries(
  id, run_id, campaign_id, ts,
  kind  enum('pledge_authorized','pledge_captured','pledge_voided','pot_credit',
             'topup','key_minted','key_limit_set','spend_settled','reserve_hold',
             'reserve_release','platform_fee','gateway_fee','sandbox_cost',
             'pause','resume','ship','refund','adjustment'),
  amount_usd numeric(12,6),       -- signed; pot-affecting kinds only
  counterparty text,              -- stripe pi_…, openrouter key hash, e2b sandbox id
  upstream text,                  -- 'anthropic' | 'amazon-bedrock' | 'google-vertex' (from generation data)
  ref jsonb                       -- generation ids, wave, agent_id, etc.
)
```

Derived views: `pot_available = Σ(pot_credit + topup) − Σ(spend_settled) − Σ(platform_fee) − Σ(refund)`. `pot_remaining_for_key = pot_available − reserve`.

Spend truth: OpenRouter key `usage` (poll `GET /api/v1/keys/{hash}` every 30–60 s; write the delta as `spend_settled`). Per-request detail comes from the transcript/OTel (§8.2) and, in Phase 2, from the proxy calling `GET /api/v1/generation?id=` for exact native cost and the serving provider.

### 5.2 Key limit ≠ pot

The spec says "USD limit = Stripe pot". Literally that overspends, because:

1. OpenRouter enforces the limit at request start against `limit_remaining`; an in-flight Fable response (up to $50/M output) can overrun by several dollars, and a wave runs many subagents in parallel.
2. Fees come off the pot before it becomes tokens (§5.4).

So: `key.limit = pot_available − reserve`, where `reserve = max($10, 3% × pot_available)` (*Proposed*; tune from observed overrun). When `loopd` sees `limit_remaining < reserve` at a wave boundary it does not start the next wave and requests `PAUSED(pot_empty)` cleanly. The key limit is the hard backstop for mid-wave. Every top-up: `PATCH limit`.

### 5.3 Stripe flow

Default (thread decision t5): **charge immediately, refund if never started.**

- **Pledge:** Stripe Checkout → PaymentIntent, automatic capture, `setup_future_usage=off_session` optional (lets a backer one-click top up later). Ledger: `pledge_captured` → `pot_credit`. Pot shown on the campaign is captured money.
- **Threshold met:** nothing happens to money; **Start run** becomes enabled for the creator. No campaign deadline is required. (*Proposed*: an optional creator-set deadline, and a platform backstop, see Q4.)
- **Never started:** full refund to every backer (Q4 sets the clock). Stripe does not return its processing fee, so Loopfund eats ~2.9% + $0.30 per pledge on dead campaigns. Budget for it; it is the price of a real pot on day one.
- **Top-up after start:** same Checkout flow; anyone can fuel, including the creator. Orchestrator raises the key limit within seconds of the webhook.
- **Refund of unspent pot** at Ship/Cancel: pro-rata by each backer's share of total pot credits. Whether refunds are net of non-returned Stripe fees is Q1/Q5.
- **Tax:** enable Stripe Tax; this is a sale of a digital service (compute) by Loopfund as merchant of record.

Alternative kept in the drawer: authorize-now/capture-at-threshold (`capture_method=manual`, `request_extended_authorization=if_available`, campaigns ≤ 30 days). No fee loss on unfunded campaigns, but it forces a deadline, a capture step with 3–5% shrinkage, and the "pot" on the page is not yet money. Switch to it only if dead-campaign refund fees become material.

### 5.5 Example-campaign defaults

From the thread's sourced receipts ($360–$865 self-reported for playable browser games): seed **Example project** campaigns with a $500 start threshold and a $1,500 goal, and show a plain "what $500 buys" line (≈ N critic waves at observed cache-heavy rates) once we have our own run data. Do not present these as real raised totals (spec: implementation notes).

### 5.4 Unit economics before any take rate

Per $100 pledged: Stripe ≈ $3.20 (2.9% + $0.30); OpenRouter credit-purchase fee 5.5% (with $0.80 minimum; platform buys credits in bulk so the minimum never applies) ≈ $5.30 of the remainder; sandbox ≈ $0.27/hr for 4 vCPU / 4 GB on E2B (≈ $6.50 per 24 h run-day) plus snapshot storage while paused. So roughly **8.5% + ~$6.50/run-day** is gone before Loopfund earns anything. The take rate (Q5) has to be decided with this in front of it; a 0% take rate is a loss.

The platform also carries **OpenRouter credit float**: we buy credits ahead of spend. Credits expire after 365 days (OpenRouter pricing page). Budget a $1–2k float for MVP.

---

## 6. Sandbox specification

**Template image** (`loopfund/gauntlet:<ver>`), built with E2B template tooling:

- Ubuntu 24.04, Node 22, pnpm, git, git-lfs, Python 3, ffmpeg, ImageMagick.
- Chromium (headless=new) with `--use-angle=swiftshader --enable-unsafe-swiftshader --ignore-gpu-blocklist` for WebGL2 without a GPU. Slow (seconds per frame), sufficient for 11-shot capture; `profile.mjs`-style frame-time numbers will not be meaningful. GPU tier (Modal/Daytona/Beam) is a later option; do not block MVP on it.
- Claude Code (native install, pinned version) and the TypeScript Agent SDK.
- `/opt/loopfund/loopd` (wave supervisor), `/opt/loopfund/tools/` (capture, shotset, baseline, imagediff, playtest, grid — adapted from CoD tooling), `/opt/loopfund/reference/` (Gauntlet Loop post, CoD README process notes).
- OTel exporter config pointing at the orchestrator's collector.

**Resources:** 4 vCPU / 8 GB RAM to start (lead + ~8 subagents + Chromium + Vite). Pause cost ≈ 4 s/GB of RAM (E2B docs) → ~30 s to pause; keep RAM at 8 GB, not 16.

**Filesystem layout** (persists across pause/resume; also mirrored to GitHub each wave):

```
/workspace/<slug>/          run repo (git; remote = platform GitHub org)
  bar/                      creator's reference artifacts (read-only bind or chmod 444)
  workbench/index.html      lead-maintained live page
  WAVE.json                 lead-reported wave summary (pieces, verdicts, scores, next gap)
  NUDGES.md                 platform-appended, read at wave start only
  ARCHITECTURE.md           lead-owned contract (if it chooses to write one; CoD pattern)
  .claude/settings.json     hooks, env, permissions (platform-owned, checked in)
  .claude/agents/           default builder.md / critic.md / smoother.md
/var/loopfund/events.jsonl  hook output, tailed by loopd
~/.claude/projects/…        session transcripts (source of per-call usage)
```

**Network:** egress allowlist only — `openrouter.ai`, `registry.npmjs.org`, `github.com`/`api.github.com` (App token scoped to this repo), the orchestrator's API/OTel endpoint, `pypi.org` optional. No inbound. E2B network policies or an egress proxy in the VM.

**Secrets in the VM:** `ANTHROPIC_AUTH_TOKEN` (the per-run OpenRouter key), GitHub App installation token (short-lived, refreshed by `loopd` from the orchestrator), `LOOPFUND_RUN_TOKEN` (scoped bearer for the orchestrator API). No platform-wide secrets. **After any break-glass attach, rotate the OpenRouter key** (mint new, patch limit, disable old) because a human could have read the env.

**Publishing from the VM:** `loopd` (not the agent) pushes `workbench/` and the 11-shot grid to the orchestrator every 5 minutes and at wave end; the orchestrator writes to R2 and serves under `/watch/<slug>`. The playable is a Vite production build uploaded the same way at wave end and at Ship.

---

## 7. Harness wiring inside the sandbox

This is where the Gauntlet Loop either survives hosting or does not.

### 7.1 `loopd` — the wave supervisor (this is `/loop`)

A ~300-line Node process, supervised by the sandbox init (restarts on crash). Pseudocode:

```
state = orchestrator.getRunState()                      // pot, paused?, nudges, session_id
loop:
  if state.paused or state.pot_remaining_for_key < reserve:
      orchestrator.requestPause(reason)                 // orchestrator pauses the sandbox; we freeze here
      continue                                          // on resume we are back here with fresh state
  appendNudges(NUDGES.md, state.nudges)                 // wave boundary is the only injection point
  result = runWave(session_id)                          // claude -p --resume <id> --output-format stream-json <wave prompt>
                                                        //   or Agent SDK query({ resume, hooks, canUseTool })
  parseTranscript(session_id) -> api_calls, agents      // §8.2
  publishWave(WAVE.json, workbench/, shots/, build/)    // orchestrator -> R2 + GitHub Release
  state = orchestrator.reportWave(result)               // returns new pot/paused/nudges
  if result.exit == 'api_error': backoff(60s→10m, cap 6) then retry same wave; after cap -> requestPause(provider)
  if result.exit == 'bar_met':   orchestrator.notifyCreator('bar met — ship?')  // loop continues unless creator ships
```

Why an external supervisor and not a Stop hook: Claude Code caps consecutive Stop-hook blocks at 8 and expects hooks to yield when `stop_hook_active` is set. A hook can guard a *turn* ("you did not write `WAVE.json`, keep going"), but the *loop across turns* has to be started from outside. That is also what makes pause/resume, nudge injection, and per-wave publishing land on a clean boundary.

Session strategy (decision, see §16 Q3): `--resume` the same session across waves for the first N waves, then start a fresh session when the transcript passes a size threshold, handing over via `WAVE.json` + `workbench/` + `ARCHITECTURE.md` + `git log`. Matt's run was one long session; a multi-day hosted run will compact repeatedly anyway, so making the durable memory *files in the repo* is safer than depending on compaction quality. Default: fresh session every wave, with the wave prompt telling the lead to read the state files. This also matches the fresh-context ethos.

### 7.2 The run prompt

Wave 1 prompt = creator goal (verbatim, from step 04) + bar description (from step 05, pointing at `bar/`) + the addendum. Waves 2+ = addendum only, plus "Continue the Gauntlet Loop from the state in `WAVE.json`, `workbench/`, and the repo."

Addendum (kept to Matt's register; ~12 lines; do not grow it):

```
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.
- Reference material on the method is in /opt/loopfund/reference/. Optional smoothing pass at
  wave end is your call.
```

### 7.3 Subagent defaults (`.claude/agents/`)

Shipped as defaults; the lead may define others. `critic.md`:

```
---
name: critic
description: Fresh-context adversarial critic. Inspects the real artifact, blind A/B vs bar/, scores 1–10, names the single biggest gap. Never edits.
model: fable          # maps to ANTHROPIC_DEFAULT_FABLE_MODEL → @preset/loopfund-critic
tools: Read, Glob, Grep, Bash
---
You are shown two sets of frames/artifacts labeled A and B. You are not told which is ours...
```

`builder.md` (model: fable or opus, all tools). `smoother.md` (optional wave-end integrator).

**Mechanical enforcement of I4** in `.claude/settings.json`:

```json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write|MultiEdit|NotebookEdit",
      "hooks": [{ "type": "command", "command": "/opt/loopfund/hooks/deny-if-critic.sh" }]
    }],
    "SubagentStart": [{ "hooks": [{ "type": "command", "command": "/opt/loopfund/hooks/emit.sh" }] }],
    "SubagentStop":  [{ "hooks": [{ "type": "command", "command": "/opt/loopfund/hooks/emit.sh" }] }],
    "PostToolUse":   [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "/opt/loopfund/hooks/emit.sh" }] }],
    "StopFailure":   [{ "hooks": [{ "type": "command", "command": "/opt/loopfund/hooks/emit.sh" }] }],
    "Stop": [{ "hooks": [{ "type": "command", "command": "/opt/loopfund/hooks/require-wave-json.sh" }] }]
  }
}
```

`deny-if-critic.sh` reads `agent_type` from stdin; if it matches `/critic/i`, returns `permissionDecision: "deny"` with the reason "critics grade; builders fix". `require-wave-json.sh` blocks a Stop **once** (honors `stop_hook_active`) if `WAVE.json` was not updated this wave. Hooks from settings run inside subagents too and carry `agent_id`/`agent_type` (Claude Code hooks reference).

### 7.4 Model routing via env + presets

In the VM env (from the OpenRouter Claude Code cookbook):

```
ANTHROPIC_BASE_URL=https://openrouter.ai/api
ANTHROPIC_AUTH_TOKEN=<per-run key>
ANTHROPIC_API_KEY=
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
CLAUDE_CODE_ENABLE_TELEMETRY=1  OTEL_METRICS_EXPORTER=otlp  OTEL_LOGS_EXPORTER=otlp  OTEL_EXPORTER_OTLP_ENDPOINT=<orchestrator>
```

Each preset (created once at `openrouter.ai/settings/presets`, Anthropic Messages skin): pinned model id (e.g. `anthropic/claude-fable-5.1`, not a `-latest` alias, so price is predictable), `provider: { order: ["anthropic","amazon-bedrock","google-vertex"], only: [same], allow_fallbacks: true }`. That is "Anthropic 1P first, failover to other Anthropic providers, never to a different model". The "429 on Anthropic-A → OpenRouter-B handoff, 0 s downtime" in mock 17 is this provider failover; the ledger's `upstream` column comes from the generation record.

**Routing optimization is a preset experiment, not a harness change.** To test "Sonnet builders, Fable critics", change what `@preset/loopfund-opus` points at for a cohort of runs and compare `Δscore per $` (§8.3). Rule 5 stands: `@preset/loopfund-critic`/fable presets are never downgraded.

Effort: set Claude Code's effort to its maximum ("ultracode" in the post) via the run's settings; confirm the exact settings key against the pinned Claude Code version when building the template.

---

## 8. Observability model

### 8.1 Who sees what

| Signal | Backer (`/watch`) | Creator console | Ops |
|---|---|---|---|
| Workbench page, 11-shot grid, playable | yes | yes | yes |
| Pot / spent / this-wave spend | yes (totals) | yes | yes |
| Critic verdicts + scores per piece | yes | yes | yes |
| Per-agent token/cost breakdown | no | yes (by role) | yes (by agent_id, model, upstream) |
| Process list, CPU/RAM (mock 14) | no | yes | yes |
| Nudge queue | no (chat votes only between waves) | yes | yes |
| Provider handoffs, 429s | no | summary | full ledger (mock 17) |
| Break-glass attach events | **never** | yes (own) | yes |
| Transcripts | no | no (MVP) | yes, audited |

### 8.2 Three spend/attribution sources, reconciled

1. **Financial truth:** OpenRouter key `usage` / `limit_remaining` (poll). What the pot is debited by.
2. **Per-call detail:** the Claude Code **session transcript JSONL** on the VM (`~/.claude/projects/<cwd>/<session>.jsonl` plus subagent transcripts). Every assistant message carries `usage` (input, output, cache_read, cache_creation) and `model`. `loopd` parses it at wave end and posts `api_calls(run, wave, agent_id, agent_type, model, tokens…, est_cost)`. Hook `SubagentStart/Stop` events give exact agent windows and types. This is the only source that reliably distinguishes *our* builder vs critic: Claude Code's OTel replaces user-defined `agent.name` values with `"custom"`, so metrics alone cannot tell them apart.
3. **Cross-check:** Claude Code OTel `claude_code.token.usage` / `cost.usage` with `model`, `type` (cacheRead ratio), `query_source` (`main|subagent|auxiliary`), `effort`. Cheap, streaming, good for live dashboards and alerting (cache-hit ratio collapse is the first efficiency signal).

Reconciliation job: Σ(est_cost from 2) vs Δ(usage from 1) per wave; flag > 5% drift. Phase 2 (§8.4) replaces estimates with exact per-generation cost.

### 8.3 Metrics that make routing optimization possible

Per run, per wave, per role, per model: tokens by type, est. USD, cache-hit ratio, critic score delta, verdict win rate, wall time, 429 count, provider mix. The one number to optimize: **critic-score delta per dollar per wave**, split by builder model. Store `bar_score_series` from `WAVE.json` so it is queryable without parsing HTML.

### 8.4 Phase 2: thin Loopfund gateway (optional, not a router)

A pass-through proxy at `ANTHROPIC_BASE_URL` that forwards to OpenRouter unchanged, and: logs every request/response `usage` with the run id; fetches `GET /api/v1/generation?id=` for exact native cost and `provider_name`; enforces a soft local budget check (so we can pause mid-wave *before* OpenRouter's 402); and can inject `provider` prefs per run for experiments without editing presets. It does not choose models. Keep it out of MVP: presets + key limits + transcript parsing already meet the success bar.

---

## 9. Nudges, votes, break-glass

- **Nudge** (creator): free text ≤ 500 chars, queued with timestamp; `loopd` appends queued entries to `NUDGES.md` at the next wave start and marks them delivered. UI copy: "Queued for the next critic round. Do not interrupt the builder." (mock 10).
- **Chat votes** (backers): between waves only, on options the lead itself listed in `WAVE.json.next_step_options` (if any). The winning option becomes one line in `NUDGES.md`. No free-text backer input reaches the harness in MVP (prompt-injection surface, and rule 3).
- **Pause / Ship** (creator): Pause = `PAUSED(creator)` at wave boundary by default; "pause now" flag pauses the sandbox immediately (memory snapshot, so nothing is lost). Ship = final capture, build, release tag `ship`, key disabled, refund calculation.
- **Break-glass** (creator, logged): E2B PTY/`exec` into the sandbox, recorded to the ledger as `break_glass` with duration, visible to creator + ops only; triggers key rotation on exit. Never shown on `/watch`.

---

## 10. Failure handling

**Provider 429 / 529.** Layered: (1) OpenRouter fails over across Anthropic providers per preset; (2) Claude Code's own retry/backoff; (3) if a turn still ends in an API error, `StopFailure` fires → `loopd` backs off and re-runs the wave with `--resume`; (4) after 6 failures → `PAUSED(provider)`, creator notified, auto-retry every 15 min. The harness process is never killed by a 429; the VM is never stopped by a 429.

**Empty pot.** Preferred path: `loopd` refuses to start a wave when `limit_remaining < reserve` → clean pause. Hard path: mid-wave 402/403 from OpenRouter → Claude Code error → `StopFailure` → `loopd` checks the key → `PAUSED(pot_empty)` → sandbox `pause()` → key disabled. Top-up → `PATCH limit` → `resume()` → `loopd` continues. Because E2B pause keeps memory, the lead's in-flight state survives; if the resume produces stale HTTP connections, Claude Code retries; if the session is unrecoverable, `loopd` falls back to `--resume <session_id>` on the same disk.

**Sandbox crash / host failure.** Repo is pushed to GitHub every wave; session transcripts mirrored to R2 every wave (Agent SDK `SessionStore` or plain file copy). Recovery = new sandbox from template, clone, restore transcript, `--resume`. Loses at most one wave.

**24 h continuous-runtime clock (E2B).** `loopd` schedules a pause/resume at the first wave boundary after 20 h. If Fly Sprites are used instead, this does not apply.

**Context compaction / long runs.** Durable memory is files (`WAVE.json`, `workbench/`, `ARCHITECTURE.md`, git history), not the transcript. See §7.1 session strategy.

**Idle / stuck.** No new shots for 2 h → creator + ops alert. No automatic stop (I5).

**Plateau checkpoint** (thread rule, t2). If critic scores in `WAVE.json` show no meaningful gain for N consecutive waves (*Proposed*: N = 5, "meaningful" = +0.2 on the 1–10 scale or any A/B win), `loopd` requests `PAUSED(plateau)` at the wave boundary and the platform asks: creator gets "Resume / Ship / Cancel"; backers get a between-waves vote on "keep going" that is advisory unless the creator is unresponsive for 72 h, in which case the backer majority decides between Resume and Cancel-with-refund. This keeps I5 (a human stops it, never a counter) while honoring the thread's "a public pot is a firehose" concern.

**Runaway spend rate.** Alert if wave cost > 3× the trailing median; still no automatic stop unless the key limit says so.

---

## 11. Episodes and distribution

- Platform GitHub org (`loopfund-runs`), one repo per run, private by default; public on Ship if the creator opted into open-source (Q9), otherwise stays private and backers get release-asset access via signed URLs.
- Release per wave (`wave-N`): body = score table + biggest gap + next step from `WAVE.json`; assets = 11-shot grid PNG, per-shot PNGs, `playable-wave-N.zip` (Vite build), `WAVE.json`. 60 s speedrun asset is Phase 3 (needs frame capture over time; `loopd` can already save a shot every 5 min to make it cheap later).
- `/watch/<slug>` reads the latest snapshot from R2; `/watch/<slug>/wave/N` is the episode page.
- Twitch: out of MVP (Q8). The 5-minute shot cadence plus the workbench is the "stream".

---

## 12. Web app surfaces and routes

Current repo is static HTML. Move to Next.js (App Router) in the same Vercel project; keep the existing CSS tokens (amber `#e09b2d`, dark workbench UI) and reuse mock PNGs as full-bleed placeholders where a screen is not yet real.

**Auth (thread t5):** creator = GitHub OAuth (required; also feeds the credentials card). Backer = guest checkout with email (Stripe Checkout collects it); optional GitHub/email login to see "my loops" and vote. Ops = platform staff SSO. The sandbox has no user login of any kind; it holds only the per-run OpenRouter key and a scoped run token.

| Route | Purpose | Data |
|---|---|---|
| `/` | Funder home (search, categories, featured) | `campaigns` |
| `/c/<slug>` | Campaign: goal, bar, credentials (mock 02 + 15), pot, backers, "Fund this loop" | `campaigns`, `pledges`, `creator_credentials` |
| `/c/<slug>/fund` | Checkout (mock 03) — copy: held for this loop, not a tip; refund terms | Stripe |
| `/watch/<slug>` | Public workbench, 11-shot grid, verdicts, spend meter, episodes list (mock 16) | R2 snapshot, `waves`, `verdicts` |
| `/watch/<slug>/wave/<n>` | Episode page | `episodes` |
| `/creators` | Marketing for creators | — |
| `/creators/new/{goal,bar,credentials,review}` | Wizard 04–05 + credentials | `campaigns` draft |
| `/creators/<slug>` | Creator panel: waiting-fuel (06) → start-run (07) → booting (08) → console (10/14) | `runs`, realtime |
| `/ops/runs/<id>` | Ledger + handoffs (mock 17), transcripts, break-glass log | everything |
| `/spec` | Partner page (this doc + SPEC.md + mock gallery) | static |
| `/api/stripe/webhook`, `/api/runs/<id>/…` (orchestrator-facing, bearer-scoped) | — | — |

Core tables: `users`, `campaigns`, `creator_credentials`, `pledges`, `runs`, `waves`, `agents`, `api_calls`, `shots`, `verdicts`, `nudges`, `episodes`, `ledger_entries`, `events`.

---

## 13. Proposed answers to the spec's open questions

All *Proposed*. None should appear in UI copy until confirmed.

1. **Legal / ToS.** Reframe internally and in copy from "escrow" to **prepaid compute purchased from Loopfund, earmarked for one loop, refundable if unused**. Loopfund is merchant of record; no funds are transmitted to the creator (withdrawal is $0 by rule 1), which removes the classic crowdfunding money-transmitter shape. Residual questions for counsel: stored-value / gift-card statutes for prepaid balances (mitigated by refund-on-unused and no expiry until settlement), consumer refund disclosure, Stripe's prohibited/restricted business list wording for crowdfunding. Refund SLA: initiate within 5 business days of Ship/Cancel; Stripe's 5–10 day settlement is outside our control and is disclosed. Avoid the word "escrow" in consumer copy; use "held for this loop".
2. **GitHub Sponsors.** Distribution channel, not competitor. Post-MVP: "Sponsor via Loopfund" badge for repos; ignore for now.
3. **Creator KYC.** MVP: GitHub OAuth required (proves account age, public repos, contribution graph shown on the credentials card), links to prior projects fetched and displayed as claimed-and-linked, not verified. Stripe Identity gated behind a pot threshold (e.g. campaigns with goals > $2,500) — flag as a knob. Manual review queue for the first 50 campaigns regardless.
4. **Threshold and cancellation.** With charge-now pledges there is no forced deadline. Backstops: if the threshold is not reached within 60 days of publish, or Start run is not clicked within 14 days of reaching it, the campaign auto-cancels and every pledge is refunded in full (Loopfund absorbs Stripe fees). Creator may cancel any time before Start run → full refunds. Backer may request a refund of their own pledge any time before Start run (self-serve). After Start run: no individual withdrawal; unspent pot refunds pro-rata at Ship/Cancel.
5. **Take rate.** Present the fee line as one number covering gateway + payment + platform. Given §5.4 (~8.5% hard cost + sandbox), a candidate all-in fee of 15% on captured funds, shown before checkout as "15% covers payment processing, gateway, and hosting; 85% becomes tokens", is the number to argue about. Do not put any percentage in the UI until decided.
6. **Failure of the loop.** MVP = pause + refund remaining, as the spec leans. Triggers: creator inactive (no console login, no nudge, no playtest) for 14 days while paused, or critic score flat/declining for 5 consecutive waves *and* creator does not respond to a 72 h notice. Never auto-kill a run that is improving.
7. **IP / known-IP.** Original games + clearly labeled homages. Reference bars may include copyrighted screenshots for the critic (private, in `bar/`, never republished in episodes or the 11-shot grid). Standard DMCA agent + takedown page. Homage labeling is a checkbox in the wizard with a plain-language warning.
8. **Twitch vs first-party.** First-party workbench only for MVP; keep the 5-min shot archive so a VOD/speedrun pipeline can be built without re-running anything.
9. **Open-source-on-ship.** Default off. If on: repo flips public at Ship with the creator's chosen license; if off: backers get signed-URL access to release assets and a hosted playable.
10. **Production vs PR.** `~/loopfunder` on the existing Vercel project is production. Close PR #1 without merging; cherry-pick anything useful by hand. Add a branch-protection rule so cloud agents open PRs against `main` and cannot deploy.

---

## 14. Gaps the spec does not mention

1. **Reserve / in-flight overrun** (§5.2). "Limit = pot" overspends without it.
2. **Fees before tokens** (§5.4). Roughly 8.5% + sandbox time is gone before any take rate; the copy "USD that can only be spent as model tokens" needs a footnote once the fee is set.
3. **OpenRouter credit float and 365-day expiry** (§5.4).
4. **The loop primitive.** Stop-hook blocking is capped at 8; the loop must be an external supervisor (§7.1). This is the single most important architectural correction.
5. **Per-agent attribution.** Claude Code OTel redacts custom agent names to `"custom"`; builder-vs-critic cost split must come from transcript parsing + hook events (§8.2).
6. **Session strategy across days** (§7.1 / §16 Q3): resume vs fresh-per-wave with file-based memory.
7. **E2B 24 h continuous-runtime clock** and the pause/resume dance around it (§10). Fly Sprites avoid it; decide by pricing and pause fidelity.
8. **Workbench serving.** Must be a snapshot on a CDN, not a live tunnel to the VM (security, and it dies on pause).
9. **Reference-bar IP hygiene.** Never publish `bar/` frames; the grid shows ours only. Mock 14 shows "BAR" thumbnails next to "OURS" — that is fine in the creator console, not on `/watch`.
10. **Key rotation after break-glass** (§6).
11. **No GPU.** SwiftShader WebGL2 is fine for stills; performance profiling numbers in CoD's README style will not be reproducible. State this in the creator wizard.
12. **Playable hosting for backers.** Needs a static build target and a signed-URL scheme; not in the spec's MVP list but implied by "early access + playable artifact".
13. **Sales tax** on a digital service (Stripe Tax).
14. **Capture shrinkage** at threshold (expired/declined cards); show the pot only after capture.
15. **Abuse.** Nudge text is a prompt-injection surface (creator is semi-trusted; backers get no free text). Egress allowlist and CPU quotas cover exfil/mining. Content policy for what the loop builds (Anthropic usage policy applies to us as the API customer).
16. **Ops runbook**: what a human does at 3 a.m. when a run is `PAUSED(provider)` for 6 h. Minimal: nothing automatic beyond retry; notify creator; no refunds triggered by provider outages.

---

## 15. Build order and test matrix

Build the machine before the marketplace. The spec's success bar is entirely about the run, not the storefront.

**Phase 0 — harness in a box (local, ~1 week).** Docker image with Claude Code, Chromium, tools, `loopd`, hooks. One OpenRouter key with `limit: 25`. Run Matt's `prompt.md` against a `bar/` of CoD frames. Verify: subagents spawn; critic denied on Edit; `WAVE.json` written; `loopd` starts wave 2 unattended; transcript parser reconciles with key `usage` within 5%; run stops only when the key hits $25. Gate: the preservation test in §1 passes.

**Phase 1 — sandbox + brakes (~1 week).** E2B template; orchestrator skeleton (state machine, key mint/patch/disable, pause/resume). Chaos tests: inject 429 via a local proxy for 10 minutes → harness survives; set key limit to $0 mid-wave → `PAUSED(pot_empty)` → `PATCH limit` → resume → same disk, same session; kill the sandbox → recover from GitHub + mirrored transcript.

**Phase 2 — money (~1 week).** Stripe manual-capture pledges, capture at threshold, top-ups, refunds, ledger, reconciliation job. Test with Stripe test cards including a decline at capture.

**Phase 3 — public surfaces (~1–2 weeks).** `/watch` from R2 snapshots, episodes via GitHub Releases, creator console (nudge, pause, ship, break-glass), campaign + checkout pages real. Wizard real.

**Phase 4 — polish to "what good looks like".** Three-click fund on an example campaign; For-creators walkthrough without a cash-out anywhere.

Test matrix against the spec's "tests that matter" plus the invariants:

| Test | Phase | Pass condition |
|---|---|---|
| Empty-pot pause/resume keeps disk | 1 | `sha256sum -r /workspace` identical before pause and after resume; session resumes |
| 429 does not kill the harness | 1 | zero `loopd` restarts; ≤ 1 wave retry; provider handoff visible in ledger |
| Key cannot spend past the pot | 1 | OpenRouter `usage ≤ limit`; reserve covers observed overrun |
| Critic never edits | 0 | hook denial count > 0 in a run where the lead tried; zero critic-authored diffs |
| Unbounded loop | 0 | ≥ 3 waves with no human input; stop reason = `pot_empty` |
| Nudge lands at wave boundary only | 3 | `NUDGES.md` mtime ∈ wave-start timestamps only |
| Backers never see break-glass | 3 | `/watch` payload contains no `break_glass` events |
| Stripe test payment → tokens | 2 | ledger chain pledge → capture → key_minted → spend_settled is unbroken |

---

## 16. Questions for Miguel

1. **Sandbox provider:** E2B (memory-preserving pause, 24 h clock, $150/mo floor) vs Fly Sprites (no cap, auto-sleep, checkpoint/restore, cheaper floor)? Default in this doc is E2B; happy to flip.
2. **Pledge model:** the thread chose charge-now/refund-if-unfunded and this doc now defaults to it. Confirm you are fine eating ~3% on campaigns that never start; if not, authorize/capture is the alternative (§5.3).
3. **Session strategy:** one long resumed session (closest to Matt's run) vs fresh session per wave with file-based memory (more robust for multi-day hosted runs)? Default: fresh per wave.
4. **Fee presentation:** one all-in percentage vs itemized (payment / gateway / platform)? And is 15% all-in the right number to start the argument from?
5. **Critic model floor:** is "critic always Fable 5.1" the rule, or "critic never below the builder's model"? The latter allows Opus critics on Opus builders for cheaper campaigns.
6. **Reference bar IP:** OK to store copyrighted screenshots privately in `bar/` for critic use, never published? If not, homages need creator-made bars.
7. **Backer votes:** in MVP at all, or drop to creator-only nudges until we see how the lead's `next_step_options` behave?
8. **Mock 17 vs MVP:** accept that MVP's ops ledger shows settled spend only (per-key polling + transcript), and that per-call "reserved → settled" rows arrive with the Phase 2 proxy? Or is per-call reserve/commit a day-one requirement (which pulls the proxy into MVP)?
9. **"Escrow" in copy:** OK to replace with "held for this loop" across the live site now, ahead of counsel?
10. **Plateau checkpoint:** is N = 5 flat waves the right trigger, and should the backer vote ever be binding (only when the creator is unresponsive, as proposed)?
11. **Disk:** the Mac was at 100%; ~1.3 GB is free after clearing two caches. Phase 0 (Docker image, Chromium) still needs ~20 GB.

---

## 17. Prototype build plan (re-sequenced to Miguel's priorities, 2 Sep)

**Stack decision: keep the static site, add a thin backend.** The high-fidelity HTML pages built today (`project`, `checkout`, `thanks`, `watch`, `onboard-*`) become the real UI; they fetch JSON from Vercel functions under `api/`. No framework migration. Supabase for Postgres + GitHub OAuth + Storage (pitch video, bar frames, workbench snapshots). Stripe Checkout (hosted) for payments. This is the shortest path to "someone can fund a real campaign with a real card" without rewriting what already renders.

| Track | Deliverable | Needs from Miguel |
|---|---|---|
| **A · Money (priority 1)** | `api/checkout` → Stripe Checkout Session (mode=payment, metadata campaign_id, 15% fee shown as a line item description, Stripe Tax on) · `api/stripe-webhook` → `ledger_entries` (pledge_captured, pot_credit) · `campaigns.pot_usd` view · `thanks.html` reads the session · refunds via `api/refund` (ops only) | Stripe account in test mode (publishable + secret + webhook secret) as Vercel env vars. Or approve using the connected Stripe MCP to set up test-mode products. |
| **B · Creator posting (priority 2)** | GitHub OAuth via Supabase Auth · `campaigns` table (title, category, pitch, destination, threshold, stretch, video_url, poster_url, bar_private[]) · Storage buckets `pitch-video` (public), `bar` (private, signed URLs for the sandbox only) · wizard pages write via `api/campaigns` · publish flips `status=FUNDING` | Supabase project (or approve creating one via the connected Supabase MCP) · GitHub OAuth app client id/secret |
| **C · Sandbox end to end (priority 3)** | Phase 0 gate on `harness/` (this commit) → E2B template from the same Dockerfile → `api/runs/start` mints key + creates sandbox + injects GOAL.md/bar/ · `loopd` posts to `api/runs/:id/wave` · `watch.html` renders from snapshots in Storage | OpenRouter **Management** key + ~$30 credits for the $25 Phase 0 run · E2B API key · ~20 GB free disk locally **or** build the image in CI/E2B instead of on the Mac |
| **D · Public read surfaces** | `index.html` lists real campaigns · `project.html` reads pot/backers/rounds · `watch.html` reads latest snapshot + WAVE.json | nothing extra |

**Order:** A and C in parallel (A is a day of Stripe wiring; C is gated on keys and disk), then B, then D. The first end-to-end demo is: a real $5 test-card pledge appears in the pot → creator clicks Start run → E2B boots → wave 1 lands on `/watch` → pot goes down.

**Data model (minimum):** `users(id, github_login, verified_at)` · `campaigns(id, slug, owner, status, title, category, pitch, destination, threshold_usd, stretch_usd, video_url, poster_url, model_plan, open_source_on_ship)` · `bar_assets(campaign_id, name, storage_path)` · `ledger_entries(id, campaign_id, run_id, ts, kind, amount_usd, counterparty, upstream, ref)` · `runs(id, campaign_id, state, sandbox_id, or_key_hash, session_id, wave, started_at)` · `waves(run_id, n, cost_usd, best_score, wave_json, release_url, snapshot_path)` · `nudges(run_id, text, queued_for_wave, delivered_at)`.

**Not in the prototype:** take-rate line in Stripe as a separate transfer (single charge, fee is internal accounting) · Stripe Identity · votes · proxy (Phase 1, per decision #5 it follows immediately after Phase 0 passes).

### 17.1 Provisioned (2 Sep, evening)

Auth moved from Supabase Auth to **Clerk** (Miguel's call). Everything below is live.

| Piece | State | Identifiers |
|---|---|---|
| Supabase | project `loopfund`, us-east-1, schema applied (`users, campaigns, bar_assets, runs, ledger_entries, pledges, waves, nudges`, view `campaign_totals`, buckets `pitch-video` public / `bar` private / `snapshots` public, RLS on; API functions use the service role) | ref `oxohstytaznkhdvuietx` |
| Clerk | app **Loopfund**. **Production instance live (2 Sep, 9:20 pm)**: `ins_3InQ8uBfa3micNywhGy56Ym4rD7`, FAPI `clerk.loopfunder.com`, five CNAMEs on Vercel DNS verified, GitHub + Google with Miguel's own OAuth credentials, email code. Vercel production env holds the `pk_live`/`sk_live` pair; preview/development stay on the dev instance. Dev-instance accounts do not carry over. | `app_3In6fHHKJXCNrKP5SKuecfey0Yh` |
| Stripe | **live** account, charges + payouts enabled. Webhook `we_1UBMeBBHlfzee9nYXjMMXze4` → `https://loopfunder.com/api/stripe-webhook` (`checkout.session.completed`, `async_payment_*`, `charge.refunded`) | `acct_1UBMU6BHlfzee9nY` |
| Vercel | 11 env vars in prod/preview/dev (`SUPABASE_*`, `CLERK_*`, `STRIPE_*`, `OPENROUTER_RUN_KEY`, `SITE_URL`, `LOOPFUND_FEE_BPS=1500`). Deployed. | `prj_u0s9gJsJkO1NXvQOT5eKLNWD9ECA` |
| OpenRouter | inference key on file ($100 credits, ~$65 remaining). It is **not** a Management key, so `mint-key.mjs` can't run yet; Phase 0 uses this key directly as `OPENROUTER_RUN_KEY`. | — |

**API surface** (`api/`, Vercel Node functions, web-standard handlers):

| Route | Auth | Does |
|---|---|---|
| `GET /api/campaigns` · `?mine=1` | public · Clerk | list published campaigns with totals · caller's drafts |
| `POST /api/campaigns` · `PATCH ?slug=` | Clerk | create draft · update; `publish:true` flips DRAFT→FUNDING (requires linked GitHub) |
| `GET /api/campaign?slug=` | public | detail + totals + backer wall + latest run/waves + owner card |
| `POST /api/checkout` | public | validates $5–$10k + email, inserts pending pledge, creates Stripe Checkout Session (85/15 split in the line-item description), returns hosted URL |
| `POST /api/stripe-webhook` | Stripe signature | marks pledge paid, writes 3 ledger rows (`pledge_captured`, `platform_fee`, `pot_credit`, idempotent on `ref`), flips FUNDING→FUNDED at threshold, handles refunds |
| `GET /api/session?id=` | public | receipt data for `/thanks`; falls back to Stripe if the webhook hasn't landed |
| `POST /api/upload` | Clerk, owner | signed direct-to-Storage upload URL for pitch video / poster (`pitch-video`) or reference frames (`bar`) |

**Verified:** signed synthetic `checkout.session.completed` → pledge `paid`, pot $4.25 / fee $0.75 on $5, wall entry, receipt; forged signature → 400. Rows deleted afterwards so the ledger starts empty for the first real charge.

### 17.3 Creator flow hardening (2 Sep, late)

- **Dashboard** `/dashboard`: lists the signed-in creator's drafts and live campaigns (`GET /api/campaigns?mine=1`), shows what a draft still needs (destination, pitch video, reference frames, goal), Continue/Publish/View actions, a Connect GitHub button when no verified GitHub account is linked, sign-out. "Log in" links site-wide now go here.
- **Save & exit** on every wizard step persists that step's fields first (`LF.saveAndExit`), then returns to the dashboard. `?new=1` starts a fresh draft; `?slug=` resumes one.
- **GitHub gating**: publishing still requires a verified GitHub account (that is the credential backers see). The 403 is now rendered as a plain sentence plus a Connect GitHub button that runs Clerk's `createExternalAccount({strategy:'oauth_github'})`. Backend now ignores unverified external accounts.
- **Clerk key** is served by `GET /api/config` from `CLERK_PUBLISHABLE_KEY`, so moving to production is: swap `CLERK_PUBLISHABLE_KEY` / `CLERK_SECRET_KEY` on Vercel and redeploy. **Still blocked on Miguel**: `clerk deploy` is interactive-only (creates the production instance, asks for the domain and GitHub OAuth client id/secret). Runbook: (1) github.com/settings/developers → New OAuth App, homepage `https://loopfunder.com`, callback `https://clerk.loopfunder.com/v1/oauth_callback`; (2) `cd ~/loopfunder && clerk deploy`; (3) I add the CNAMEs it prints to Vercel DNS and push the `pk_live_`/`sk_live_` keys.
- **Copy**: "loop" jargon removed from creator and backer surfaces ("Fund this project", "Back this project", "Projects funding now", "Start the build"). "Gauntlet Loop" survives only in the spec/implementation pages.
- **Homepage**: featured block is now data-driven (live campaign with a pitch video and the largest pot), a real card with poster, progress, days left and two CTAs; stacks cleanly on phones. Search box and category tabs filter the campaign list (`/?q=…`, `/?cat=Game`), live while typing, shareable URL.

### 17.4 Marketing redesign + /mvp spec page (2 Sep, 7:30 pm)

- **Type system.** Fraunces + Outfit replaced by Bricolage Grotesque (display, 700–800, tight) + IBM Plex Sans (body) + IBM Plex Mono (money figures), exposed as `--display / --body / --mono` in `site.css` and consumed by every page; old inline `Fraunces,serif` references rewritten to the variables. Fonts load from Google Fonts via one `@import`; Plex Mono stays self-hosted.
- **Home.** Hero is now the featured campaign's own pitch video (muted autoplay, poster fallback) with the campaign card beside it; a one-line site descriptor above it. Screenshot-based "How funding works / Creator path" rows (stale mocks that still said "Fuel the loop") are gone, replaced by a *Where your money goes* 85/15/0 bar, three plain steps, and a creator band. Search/category filtering unchanged; filtering hides the marketing sections.
- **/creators.** Rewritten: headline + lede, a *you bring / backers bring / you get / you don't get* ladder, four-step timeline, six FAQs, CTA band. No "loop" anywhere.
- **/mvp** now points at `mvp.html`: one-page spec and status (what it is, status board, phases with checkboxes, surfaces + all 17 mocks with live/mock tags, state machine, money, architecture, the build loop, decisions, open questions, next steps, test matrix). `spec.html` remains as the original partner spec.
- Styles for the two marketing pages live in `css/home.css`; dead `.hero-art/.featured` rules removed from `site.css`.

### 17.5 Metadata, favicon, per-project link previews (2 Sep, 7:45 pm)

- Favicon set: `favicon.svg` (amber tile, black loop mark), `favicon-32.png`, `apple-touch-icon.png`, `site.webmanifest`, theme color. Default share card `og/default.png` (1200×630, rendered from the site's own type). OG + Twitter tags on every static page.
- **Canonical campaign URL is now `/projects/:slug`** (`/project?slug=` still works). Both route to `api/project-page.js`, which reads `templates/project.html` (bundled via `includeFiles`) and injects the campaign's own `<title>`, description (whole sentences plus "Funding: $x of $y"), `og:image` = poster, `og:video` = pitch mp4 when present, canonical link; 404 for unknown/draft slugs, redirect to `/` with no slug. The template had to move out of the web root because `cleanUrls` matched `project.html` on disk before the rewrite ran.
- All in-site campaign links use the canonical path; the template's asset paths are absolute so it works under the nested route.

### 17.6 Tagline + brand kit (2 Sep, 7:50 pm)

- Tagline changed from "Back a game. Pay for the machine, not the maker." (read as a riddle without the crowdfunding frame) to **"Back a game. Watch it get built."**, always followed by the money explainer. Applied to home title/description/OG, manifest, share card.
- New `/brand` page: one-breath idea, positioning + competitor table, 5 s / 30 s / 2 min pitches, tagline rationale + alternates, voice rules with not-this/this pairs, say/don't-say vocabulary, how we talk about money and about the AI, mark, palette with roles, type roles and scale, imagery/layout rules, boilerplate.
- Page roles stated on `/mvp`: `/mvp` = what and where it stands; `/implementation` = how the machine runs; `/brand` = how we talk and look.

### 17.2 Second seeded campaign: Tidewater (2 Sep, night)

A believable second campaign so the index isn't a one-card grid, and a rehearsal of the "creator brings target frames" story.

- **Stills** (4, 16:9): built-in image generator; hero + 3 gameplay frames with a diegetic HUD (compass, `SAIL %`, fish count, hull bar, `E Moor` / `E Reef sail` prompts), all conditioned on the hero for style consistency. Stored at `pitch-video/tidewater/{hero,shot-cave,shot-dock,shot-storm}.png`.
- **Clip** (15.5 s, 1280×720): two 8 s image-to-video jobs through **OpenRouter's video API** (`POST /api/v1/videos`, model `kwaivgi/kling-v3.0-std`, `frame_images[first_frame]` = hero / storm still, no audio) at **$0.672 each**, stitched with ffmpeg (0.6 s xfade). Stored at `pitch-video/tidewater/demo.mp4`, set as `campaigns.video_url`. Higgsfield MCP was dead (token revoked; needs remove + re-add in Cursor) so it wasn't used.
- **Row:** slug `tidewater`, $750 / $4,000, `bar_private=false`, `model_plan.gallery[]` holds the four frames with labels and `model_plan.demo_note` says plainly the frames are AI-generated targets, not a build. `project.html` now renders `model_plan.gallery` in "The bar" section (2×2 when >2) and shows the note; the old campaign keeps the static mock.
- **Reusable:** the same OpenRouter video path is available to creators later (pitch clip from their own frames, ~$0.70 per 8 s) and is billable through the same key/ledger as the loop itself.

**Pages wired:** `index` (live grid from `/api/campaigns`), `project?slug=` (pot, meter, backers, deadline, creator card, wall; run-only sections hidden until a run exists), `checkout?slug=` (real Stripe Checkout), `thanks?session_id=`, `onboard-goal` / `onboard-bar` / `onboard-fuel` (Clerk-gated, drafts saved via API, direct uploads, publish). `onboard-start/boot/console` and `watch` remain mock until Track C.

---

## 18. Research notes (2 Sep 2026, primary docs fetched live)

Verdicts on the four load-bearing assumptions, and what changed in `harness/` because of them.

| Assumption | Verdict | Source |
|---|---|---|
| Claude Code works unmodified against OpenRouter via `ANTHROPIC_BASE_URL` | **Confirmed.** Three env vars (`ANTHROPIC_BASE_URL=https://openrouter.ai/api` with no `/v1`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_API_KEY=""`), `/logout` first. OpenRouter: "only guaranteed to work with the Anthropic first-party provider." All four model slots must be set or aux calls 404 intermittently. Fast-mode org check calls `api.anthropic.com` directly → `CLAUDE_CODE_SKIP_FAST_MODE_ORG_CHECK=1` in the sandbox. | openrouter.ai/docs/cookbook/coding-agents/claude-code-integration |
| Subagent model can be an OpenRouter preset string | **Plausible, not primary-documented.** `@preset/slug` is a documented model-field value; presets can be created for the Anthropic skin; Claude Code passes unknown model IDs through and agent `model:` frontmatter accepts full IDs. No official example puts `@preset/` in `ANTHROPIC_DEFAULT_*`. Phase 0 test #0 = one request per slot. Fallback = full slug + account-wide provider allowlist. Request params shallow-merge over preset params. | openrouter.ai/docs/features/presets · code.claude.com/docs/en/sub-agents |
| Management API keys support USD `limit` + `disabled` | **Confirmed.** `limit` (USD), `limit_reset`, `disabled`, `include_byok_in_limit`, `expires_at`; key objects expose `limit_remaining`, `usage`. Exhausted limit → **402**. Management-API rate limits are undocumented. | openrouter.ai/docs/features/provisioning-api-keys · /docs/api_reference/limits |
| Stop hooks are capped, so the loop needs an external supervisor | **Confirmed.** "Claude Code overrides the hook and ends the turn after 8 consecutive blocks" (also for `additionalContext` and `SubagentStop`). Stop input carries `session_crons` (from `/loop`) and `background_tasks`; the wave guard now yields when either is non-empty. `StopFailure` fires instead of Stop on API errors with `error ∈ {rate_limit, overloaded, billing_error, …}`; `loopd` reads it to classify failures. | code.claude.com/docs/en/hooks |

Other facts that changed the design:

- **Hooks give exact attribution.** `SubagentStart` → `agent_id`, `agent_type`; `SubagentStop` → `agent_transcript_path`. `transcript.mjs` now joins on the transcript path. OTel `agent.name` is confirmed redacted to `"custom"` for user-defined agents; `query_source ∈ {main, subagent, auxiliary}` and `effort` survive. Claude Code also sends `x-claude-code-agent-id` / `x-claude-code-parent-agent-id` headers to the gateway, which the Phase 1 proxy can read for per-call attribution (another reason decision #5 is right). Whether OpenRouter maps `x-claude-code-session-id` to its sticky-routing `session_id` is undocumented.
- **Caching.** Claude Code sends `cache_control` natively on the Messages skin; Anthropic, Bedrock and Vertex all honor it (1 h TTL too). OpenRouter uses provider sticky routing; a failover to Bedrock costs one cache miss. A stripped marker fails silently (bills as uncached) → watch `cache_read_input_tokens` per wave.
- **Cost.** `usage: {include:true}` is deprecated; per-request USD on the Anthropic skin comes from `GET /api/v1/generation?id=` (`total_cost`, `provider_name`, `preset_id`). Credit purchase fee 5.5% ($0.80 min), credits may expire after one year: §5.4 numbers stand.
- **Effort.** `--effort` accepts `ultracode` ("xhigh with ultracode on"); Matt recommends it for serious loops. `loopd` defaults to `max`, `LOOPFUND_EFFORT` overrides.
- **Subagents.** Fresh context per subagent by construction; subagents can spawn subagents three layers deep; they inherit the main conversation's thinking config. `CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1` exists but would override critics too, so we don't use it.
- **Known gateway issues** (all with non-Anthropic models): adaptive thinking 400s/hangs (`CLAUDE_CODE_DISABLE_THINKING=1` workaround), `reasoning_effort` + `thinking:disabled` on the subagent path, rewritten context-limit errors breaking auto-compact (`CLAUDE_CODE_AUTO_COMPACT_WINDOW`). None should trigger with Anthropic 1P pinned first.

What Matt's own material says about the method (feeds `WAVE_PROTOCOL.md` and the reference dir baked into the image):

- One prompt, one long Claude Code session, "many hours", Opus 5 (now Fable 5.1 for us), ultracode, `/loop` inside the prompt, subagents for fresh-context critics. No Stop hook, no custom agent files; the lead chose how to spawn.
- Critic contract: "give it the goal, the bar, the relevant rules, and the actual artifact. Do not give it the builder's history" · "behave almost like an A/B tester … without being told which is which" · "never grade a summary written by the builder."
- Process finding from the Claude-of-Duty README: **sequential single-owner passes beat parallel fan-out** on coupled systems (+1.00 vs +0.46, defects 66 → 26 vs 60 → 66), and captures must be reproducible (a shared page across shots leaked state; 10 of 11 shots differed between identical runs). Our `shotset` opens a fresh page per angle for this reason. Scores went 3.59 → 4.14 → 4.05 → 5.05 /10 and every critic in every blind A/B still picked CoD.
- Spend: Matt has not disclosed. One community run: 51.3 h, 996 agents, 264 M tokens. At Fable list prices that is a four-figure run, which is the product thesis.

Sandbox (E2B vs Daytona, official docs): E2B pauses with memory (~4 s/GiB), paused sandboxes kept indefinitely, continuous runtime **24 h on Pro ($150/mo), 1 h on Hobby**, resume resets the clock → Phase 0 on Hobby needs a pause/resume every hour (`loopd` can do this at wave boundaries; waves are ~2 h, so Pro is needed for real runs). Daytona: no wall-clock cap with auto-stop 0, per-second billing, no subscription, but **container sandboxes don't preserve memory on pause**; only the VM class does. Both ≈ $0.05/vCPU-h + $0.016/GiB-h. E2B stays the default; Daytona VM class is the fallback.

---

## Appendix A. Neighbors and category rank (from the thread's research, t33)

Closest existing things, none of which escrow dollars into a hosted, critic-inspected frontier loop:

- **API-credit grants**: OpenAI Researcher Access (≤ $1k credits), Anthropic External Researcher Access (~$1k), Hugging Face Spaces grants. Gated subsidy, not a fan pool.
- **Community GPU for one training run**: Prime Intellect (SYNTHETIC-2, INTELLECT), Nous Psyche, Bittensor Templar, Pluralis Agora. Pools FLOPs; artifact is a checkpoint, not a watchable loop.
- **Prepaid compute wallets**: Vast.ai, RunPod Community, Venice DIEM, on-chain inference markets. You rent your own box; nothing is earmarked to someone else's project.
- **Cash to the creator**: GitHub Sponsors, Patreon, Kickstarter, OpenCollective, VitaDAO (VDP-162 earmarked $50k of Prime Intellect compute, the nearest DeSci cousin). Money lands as cash or as someone's personal quota; no pause-on-empty, no bound spend.

Category rank against "visual critic + public workbench + $360–$865 loop": (1) games, (2) interactive tools/toys, (3) movies/episodic/animation, (4) open-source product loops with tests + screenshots as the bar, (5) computational science only with a public eval, (6) frontier-model development (wrong resource; GPU pools already own it), (7) cure funding (wet lab is not a Gauntlet Loop). Matches spec rule 8.

Steelman kept from the thread (t9), because it is the bar the product has to clear: GitHub Sponsors + an MIT repo already exist; a single patron can fund a game-scale loop with Venmo; a personal card is discipline and a public pot is a firehose; if the next Max plan includes more usage the cliff shrinks. The only answers that Sponsors cannot give are the four in spec rules 1–4: money bound to a run, upstream swap without killing the harness, critics on pixels not vibes, pause instead of death when the funder goes broke.
