eleven / ocho Public
π€ 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_dayChosen 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 spacesAwake-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 nothingRevenue 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 samespacesledger asentitlement='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 aspressure < 0.8, exposed read-only (GET /api/pool) so the pricing surfaces and the status page share one source of truth.
Phasing
- PR 1 β the gauge, no behavior change. Parse
LCHAT_POOL_*+net_eur_year; compute spend/budget/pressure in the meter sweep;pool_metertable; log line; admin-tokenGET /api/pool. Ships dark (pool off unlessLCHAT_POOL_MAX_SHAREis set). Pure budget-math unit tests + a seeded meter-sweep test. - PR 2 β admission control.
free_pool_fullatpressure β₯ 1increateSpaceHandler(free creates only); home relays the message;LCHAT_POOL_DRY_RUNgates. Tests mirror the createBudget ones. - PR 3 β demo integration. Carve-out budget, pressure-tuned TTL. (Sequenced with the demo doc's PRs.)
- PR 4 β measurement. controld stats gain
awake; the meter samples a trailing per-spaceawake_frac; measured beats assumed. Optional idle-window tightening at amber. - 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.