eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

πŸ€– 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.