# 🤖 The free pool — a cost-governed budget for free & demo spaces

Design doc, 2026-07-14. Status: **designed, not built.** Companion docs:
`docs/demo-spaces.md` (the demo tier the pool contains) and
`docs/pricing-bands.md` (the pricing that funds it). This doc extends the
`docs/tier-economics.md` discipline: **formulas are the contract, numbers are
a dated snapshot.**

## The idea in one paragraph

Run the free tier as an **error budget for money**. A small accounting loop
inside the cloud provisioner's existing 6-hour meter sweep computes (a) the
estimated daily cost of every free space and demo from a tiny published cost
model, (b) a daily free-pool *budget* derived from a percent-of-total-cost
ceiling, net revenue, and an operator floor, and (c) one scalar,
`pressure = spend / budget`. Pre-agreed thresholds on that scalar drive
graceful degradation — shorter demo TTLs, then a pause on **new** free
creates — and **never touch an existing space**. Demos get a carve-out funded
first from the floor, so the try-it path survives even a red pool; only
physical box limits can ever actually block a demo. The pool is
revenue-indexed — paid spaces fund it — and the gauge is publishable on the
status page next to the books, making the subsidy a checkable claim.

Survey honesty: no SaaS publicly runs a free tier as an explicit
%-of-cost invariant. Every *part* is established — SRE error budgets with
pre-committed exhaustion policies, cloud budget actions that actuate,
Borg/spot-style surplus tiers, the Neon/Render/Heroku-Eco idle-suspend +
suspend-don't-bill pattern, and the FCC Universal Service Fund as a real
revenue-indexed pool (whose uncapped ratio ratcheting to ~38% is precisely
the failure the X-ceiling here prevents). The composition is novel; every
part is boring.

## The budget formula

```
net_revenue_day = Σ over paid ledger spaces of net_eur_year / 365   (smoothed ~90d)

invariant_cap   = (X/100) × total_cost_day        # free never exceeds X% of total cost
revenue_pool    = (Y/100) × net_revenue_day       # the growth engine
base_floor      = LCHAT_POOL_BASE_EUR_DAY         # a pool exists at zero revenue

budget_day      = min( invariant_cap , max(base_floor, revenue_pool) )
demo_budget     = (D/100) × budget_day            # reserved for demos, funded first
pressure        = free_cost_day / budget_day
```

**Chosen parameters (Paul, 2026-07-14):** `X = 25`, `Y = 30`,
`floor = €0.20/day`, `D = 20`. All config; changing them is philosophy, not
engineering.

Shape notes:

- **X is a ceiling, revenue is the engine, the floor is the bootstrap.** At
  zero revenue the sunk box subsidises the free tier (the spot-capacity
  logic: the capacity is already paid for). As revenue grows, the Y-term
  takes over. X clamps everything — the anti-ratchet guard: the ratio can
  never float upward with demand; admission control absorbs excess demand
  instead.
- **Smoothing:** revenue is computed over trailing ~90 days of entitlement
  state, so one big customer churning shrinks the pool gradually — and a
  shrink only ever gates *admissions*; existing spaces are grandfathered by
  construction, because no actuator touches them.
- Demos are the cheapest thing offered (~€0.01 lifetime each), so the demo
  carve-out is small in euros and huge in units.

## The cost model

Per instance and day — awake time, storage, noise:

```
cost_day(i) = awake_frac(i) × (C_slot + C_req)   # RAM/CPU share + S3 request tail while awake
            + stored_gb(i)  × C_store            # S3 replica (× retention) or disk share
            + C_noise                            # registry row, cert, wakes (≈ 0)

free_cost_day  = Σ cost_day(i) over spaces with entitlement in ('free','demo')
total_cost_day = C_fixed + Σ cost_day(i) over ALL ledger spaces
```

Awake-hours (wall-clock) is deliberately the metered thing, not "activity":
utilization thresholds spawn keep-alive arms races (Oracle's free-tier idle
detector did); wall-clock awake time is what the activator already measures,
and the one gaming vector — holding a WebSocket open — makes that space
expensive *on paper*, which is exactly what a later per-space awake-hours cap
would key on.

Snapshot constants (2026-07, re-measure at tier-economics re-derivations):

| Constant | Placeholder | Source |
|---|---|---|
| `C_fixed` | ~€1.17/day | whole cloud plane: box + router/home/push/cloud |
| `C_slot` | ~€0.027/day | box ÷ 40-instance materialized capacity |
| `C_req` | ~€0.002/day | post-#836 always-awake S3 request tail |
| `C_store` | ~€0.0014/GB-day | S3 × ~2 Litestream retention |
| `awake_frac` free | 0.05 (assumed) | hibernate-by-default |
| `awake_frac` demo | 0.30 (assumed) | 24h life, actively poked |

Worked typicals: a hibernation-dominant free space costs **~€0.6–0.9/year**;
a 24h demo **~€0.01 total**; a Community & Friends subscription nets
~€39/year. The arithmetic is comfortable — even a 30% revenue share funds
tens of free spaces per C&F subscription. At today's one-box scale the euro
budget is *not* the binding constraint (RAM slots and disk are); the pool
exists to be legible now and binding at scale.

Deriving cost from a config-declared model instead of metering is a
deliberate trade: real per-instance cost attribution is heavy machinery for
cents. It means the gauge is an **estimate and must say so wherever
published.**

## The control loop

Measured every existing meter sweep (6h) from the ledger + cost model; one
row per sweep in a new additive `pool_meter` table
(`ts, free_count, demo_count, paid_count, free_cost_day, total_cost_day,
budget_day, pressure`, pruned to ~a year). Creates are gated against the
latest reading; spikes are caught by the existing create breaker and
controld's physical caps, and a correlated wake-storm shows up next sweep as
a fast burn.

**The pre-agreed policy** (written before the budget is ever spent —
SRE-style):

| Pressure | State | Behavior |
|---|---|---|
| < 0.8 | green | nothing. Surplus exists — this is the "free capacity available to anyone" signal `docs/pricing-bands.md` consumes |
| 0.8 – 1.0 | amber | new demos get shorter TTL (24h → ~8h); status gauge amber; loud log line per sweep |
| ≥ 1.0 | red | **pause new free creates** — stable error `free_pool_full`, honest copy below. Demos continue at amber settings |
| ≥ 1.5 | deep red | demos shorten further / small retry-wait. Existing spaces: still untouched |

**Never actuated, ever:** suspending or deleting an existing space because of
pool pressure. The only lifecycle ends remain the reaper's never-set-up rule
and the demo TTL — pre-declared lifecycle, not budget enforcement.

At red, a free create answers `503 free_pool_full`:

> "The free pool is full right now. Paying supports free spaces for those
> who can't — as more people upgrade, the pool grows. Check back soon, or
> upgrade to create a space now."

- Paid creates are never pool-gated (they *fund* the pool); the upgrade path
  stays open at red.
- **No waitlist** (decided): accounts are anonymous passkeys with no contact
  channel, and the survey found no product running a free-tier waitlist.
  Honest-no + the live public gauge instead — a refusal that's a checkable
  claim, not a shrug.
- Self-reclaim of the account's own dead slot still works at red (it creates
  no net-new spend).

## Messaging (decided 2026-07-14)

**Aggregate form only.** The canonical line is:

> **"Paying supports free spaces for those who can't."**

The gauge publishes the aggregate ("paid spaces currently fund the free pool
M× over"), never a per-space "your €5 funds these two spaces" — the personal
link isn't mechanically true and invites weird expectations. M is re-derived
with the tier-economics numbers and is verifiable against the public books.

## Knobs and placement

```
LCHAT_POOL_MAX_SHARE=25          # X (0/unset = pool off)
LCHAT_POOL_REVENUE_SHARE=30      # Y
LCHAT_POOL_BASE_EUR_DAY=0.20     # floor
LCHAT_POOL_DEMO_SHARE=20         # D
LCHAT_POOL_COSTS="fixed_day=1.17,slot_day=0.027,req_day=0.002,storage_gb_day=0.0014,awake_free=0.05,awake_demo=0.30"
LCHAT_POOL_DRY_RUN=1             # rollout: compute, log, publish — gate nothing
```

Revenue input: an additive `net_eur_year` field per entry in the existing
`LCHAT_STRIPE_PRICES` JSON (unknown fields are ignored by today's decode;
entries without it contribute 0 and log a warning). With pricing bands
(`docs/pricing-bands.md`), each band×tier entry carries its own
`net_eur_year` — discounted spaces grow the pool exactly in proportion to
what they actually pay, which is also the honest version of the marketing
line. Once Stripe Tax ships (tax-inclusive prices), `net_eur_year` figures
are net-of-VAT approximations; the pool tolerates that error comfortably.

| Layer | Enforces |
|---|---|
| home | nothing pool-related (IP/PoW gates are the demo doc's) |
| cloud | the pool: budget math, pressure, `free_pool_full` admission, demo carve-out + dynamic TTL, `pool_meter`, the reap |
| controld | physical capacity only; the `awake` stat (phase 4). Never learns about money |
| router/activator | hibernation; (phase 4) per-entitlement idle-window override |

Existing knobs keep their jobs: `LCHAT_CLOUD_MAX_CREATES_PER_DAY` (blunt
spike breaker), `LCHAT_CLOUD_QUOTA`, the physical caps, `LCHAT_POW_BITS`,
the reaper family, storage caps.

## Interfaces

- **Demos** (`docs/demo-spaces.md`): live in the same `spaces` ledger as
  `entitlement='demo'`; draw from the carve-out; TTL and live-cap are
  pressure-tunable; **never S3-replicate** (decided — the flag is the demo
  doc's PR 1).
- **Pricing** (`docs/pricing-bands.md`): the pool consumes one number per
  paid space (`net_eur_year`). "Free capacity exists" is defined as
  `pressure < 0.8`, exposed read-only (`GET /api/pool`) so the pricing
  surfaces and the status page share one source of truth.

## Phasing

1. **PR 1 — the gauge, no behavior change.** Parse `LCHAT_POOL_*` +
   `net_eur_year`; compute spend/budget/pressure in the meter sweep;
   `pool_meter` table; log line; admin-token `GET /api/pool`. Ships dark
   (pool off unless `LCHAT_POOL_MAX_SHARE` is set). Pure budget-math unit
   tests + a seeded meter-sweep test.
2. **PR 2 — admission control.** `free_pool_full` at `pressure ≥ 1` in
   `createSpaceHandler` (free creates only); home relays the message;
   `LCHAT_POOL_DRY_RUN` gates. Tests mirror the createBudget ones.
3. **PR 3 — demo integration.** Carve-out budget, pressure-tuned TTL.
   (Sequenced with the demo doc's PRs.)
4. **PR 4 — measurement.** controld stats gain `awake`; the meter samples a
   trailing per-space `awake_frac`; measured beats assumed. Optional
   idle-window tightening at amber.
5. **PR 5 — publish.** The gauge on the status page next to the books
   ("the free pool: N spaces, funded M× over, state: green"), estimate
   caveat included.

Each PR is independently revertible; nothing before PR 2 can refuse anyone.
The rollout follows the reaper's proven dry-run-then-arm pattern.
