| 1 | # π€ The free pool β a cost-governed budget for free & demo spaces |
| 2 | |
| 3 | Design doc, 2026-07-14. Status: **designed, not built.** Companion docs: |
| 4 | `docs/demo-spaces.md` (the demo tier the pool contains) and |
| 5 | `docs/pricing-bands.md` (the pricing that funds it). This doc extends the |
| 6 | `docs/tier-economics.md` discipline: **formulas are the contract, numbers are |
| 7 | a dated snapshot.** |
| 8 | |
| 9 | ## The idea in one paragraph |
| 10 | |
| 11 | Run the free tier as an **error budget for money**. A small accounting loop |
| 12 | inside the cloud provisioner's existing 6-hour meter sweep computes (a) the |
| 13 | estimated daily cost of every free space and demo from a tiny published cost |
| 14 | model, (b) a daily free-pool *budget* derived from a percent-of-total-cost |
| 15 | ceiling, net revenue, and an operator floor, and (c) one scalar, |
| 16 | `pressure = spend / budget`. Pre-agreed thresholds on that scalar drive |
| 17 | graceful degradation β shorter demo TTLs, then a pause on **new** free |
| 18 | creates β and **never touch an existing space**. Demos get a carve-out funded |
| 19 | first from the floor, so the try-it path survives even a red pool; only |
| 20 | physical box limits can ever actually block a demo. The pool is |
| 21 | revenue-indexed β paid spaces fund it β and the gauge is publishable on the |
| 22 | status page next to the books, making the subsidy a checkable claim. |
| 23 | |
| 24 | Survey honesty: no SaaS publicly runs a free tier as an explicit |
| 25 | %-of-cost invariant. Every *part* is established β SRE error budgets with |
| 26 | pre-committed exhaustion policies, cloud budget actions that actuate, |
| 27 | Borg/spot-style surplus tiers, the Neon/Render/Heroku-Eco idle-suspend + |
| 28 | suspend-don't-bill pattern, and the FCC Universal Service Fund as a real |
| 29 | revenue-indexed pool (whose uncapped ratio ratcheting to ~38% is precisely |
| 30 | the failure the X-ceiling here prevents). The composition is novel; every |
| 31 | part is boring. |
| 32 | |
| 33 | ## The budget formula |
| 34 | |
| 35 | ``` |
| 36 | net_revenue_day = Ξ£ over paid ledger spaces of net_eur_year / 365 (smoothed ~90d) |
| 37 | |
| 38 | invariant_cap = (X/100) Γ total_cost_day # free never exceeds X% of total cost |
| 39 | revenue_pool = (Y/100) Γ net_revenue_day # the growth engine |
| 40 | base_floor = LCHAT_POOL_BASE_EUR_DAY # a pool exists at zero revenue |
| 41 | |
| 42 | budget_day = min( invariant_cap , max(base_floor, revenue_pool) ) |
| 43 | demo_budget = (D/100) Γ budget_day # reserved for demos, funded first |
| 44 | pressure = free_cost_day / budget_day |
| 45 | ``` |
| 46 | |
| 47 | **Chosen parameters (Paul, 2026-07-14):** `X = 25`, `Y = 30`, |
| 48 | `floor = β¬0.20/day`, `D = 20`. All config; changing them is philosophy, not |
| 49 | engineering. |
| 50 | |
| 51 | Shape notes: |
| 52 | |
| 53 | - **X is a ceiling, revenue is the engine, the floor is the bootstrap.** At |
| 54 | zero revenue the sunk box subsidises the free tier (the spot-capacity |
| 55 | logic: the capacity is already paid for). As revenue grows, the Y-term |
| 56 | takes over. X clamps everything β the anti-ratchet guard: the ratio can |
| 57 | never float upward with demand; admission control absorbs excess demand |
| 58 | instead. |
| 59 | - **Smoothing:** revenue is computed over trailing ~90 days of entitlement |
| 60 | state, so one big customer churning shrinks the pool gradually β and a |
| 61 | shrink only ever gates *admissions*; existing spaces are grandfathered by |
| 62 | construction, because no actuator touches them. |
| 63 | - Demos are the cheapest thing offered (~β¬0.01 lifetime each), so the demo |
| 64 | carve-out is small in euros and huge in units. |
| 65 | |
| 66 | ## The cost model |
| 67 | |
| 68 | Per instance and day β awake time, storage, noise: |
| 69 | |
| 70 | ``` |
| 71 | cost_day(i) = awake_frac(i) Γ (C_slot + C_req) # RAM/CPU share + S3 request tail while awake |
| 72 | + stored_gb(i) Γ C_store # S3 replica (Γ retention) or disk share |
| 73 | + C_noise # registry row, cert, wakes (β 0) |
| 74 | |
| 75 | free_cost_day = Ξ£ cost_day(i) over spaces with entitlement in ('free','demo') |
| 76 | total_cost_day = C_fixed + Ξ£ cost_day(i) over ALL ledger spaces |
| 77 | ``` |
| 78 | |
| 79 | Awake-hours (wall-clock) is deliberately the metered thing, not "activity": |
| 80 | utilization thresholds spawn keep-alive arms races (Oracle's free-tier idle |
| 81 | detector did); wall-clock awake time is what the activator already measures, |
| 82 | and the one gaming vector β holding a WebSocket open β makes that space |
| 83 | expensive *on paper*, which is exactly what a later per-space awake-hours cap |
| 84 | would key on. |
| 85 | |
| 86 | Snapshot constants (2026-07, re-measure at tier-economics re-derivations): |
| 87 | |
| 88 | | Constant | Placeholder | Source | |
| 89 | |---|---|---| |
| 90 | | `C_fixed` | ~β¬1.17/day | whole cloud plane: box + router/home/push/cloud | |
| 91 | | `C_slot` | ~β¬0.027/day | box Γ· 40-instance materialized capacity | |
| 92 | | `C_req` | ~β¬0.002/day | post-#836 always-awake S3 request tail | |
| 93 | | `C_store` | ~β¬0.0014/GB-day | S3 Γ ~2 Litestream retention | |
| 94 | | `awake_frac` free | 0.05 (assumed) | hibernate-by-default | |
| 95 | | `awake_frac` demo | 0.30 (assumed) | 24h life, actively poked | |
| 96 | |
| 97 | Worked typicals: a hibernation-dominant free space costs **~β¬0.6β0.9/year**; |
| 98 | a 24h demo **~β¬0.01 total**; a Community & Friends subscription nets |
| 99 | ~β¬39/year. The arithmetic is comfortable β even a 30% revenue share funds |
| 100 | tens of free spaces per C&F subscription. At today's one-box scale the euro |
| 101 | budget is *not* the binding constraint (RAM slots and disk are); the pool |
| 102 | exists to be legible now and binding at scale. |
| 103 | |
| 104 | Deriving cost from a config-declared model instead of metering is a |
| 105 | deliberate trade: real per-instance cost attribution is heavy machinery for |
| 106 | cents. It means the gauge is an **estimate and must say so wherever |
| 107 | published.** |
| 108 | |
| 109 | ## The control loop |
| 110 | |
| 111 | Measured every existing meter sweep (6h) from the ledger + cost model; one |
| 112 | row per sweep in a new additive `pool_meter` table |
| 113 | (`ts, free_count, demo_count, paid_count, free_cost_day, total_cost_day, |
| 114 | budget_day, pressure`, pruned to ~a year). Creates are gated against the |
| 115 | latest reading; spikes are caught by the existing create breaker and |
| 116 | controld's physical caps, and a correlated wake-storm shows up next sweep as |
| 117 | a fast burn. |
| 118 | |
| 119 | **The pre-agreed policy** (written before the budget is ever spent β |
| 120 | SRE-style): |
| 121 | |
| 122 | | Pressure | State | Behavior | |
| 123 | |---|---|---| |
| 124 | | < 0.8 | green | nothing. Surplus exists β this is the "free capacity available to anyone" signal `docs/pricing-bands.md` consumes | |
| 125 | | 0.8 β 1.0 | amber | new demos get shorter TTL (24h β ~8h); status gauge amber; loud log line per sweep | |
| 126 | | β₯ 1.0 | red | **pause new free creates** β stable error `free_pool_full`, honest copy below. Demos continue at amber settings | |
| 127 | | β₯ 1.5 | deep red | demos shorten further / small retry-wait. Existing spaces: still untouched | |
| 128 | |
| 129 | **Never actuated, ever:** suspending or deleting an existing space because of |
| 130 | pool pressure. The only lifecycle ends remain the reaper's never-set-up rule |
| 131 | and the demo TTL β pre-declared lifecycle, not budget enforcement. |
| 132 | |
| 133 | At red, a free create answers `503 free_pool_full`: |
| 134 | |
| 135 | > "The free pool is full right now. Paying supports free spaces for those |
| 136 | > who can't β as more people upgrade, the pool grows. Check back soon, or |
| 137 | > upgrade to create a space now." |
| 138 | |
| 139 | - Paid creates are never pool-gated (they *fund* the pool); the upgrade path |
| 140 | stays open at red. |
| 141 | - **No waitlist** (decided): accounts are anonymous passkeys with no contact |
| 142 | channel, and the survey found no product running a free-tier waitlist. |
| 143 | Honest-no + the live public gauge instead β a refusal that's a checkable |
| 144 | claim, not a shrug. |
| 145 | - Self-reclaim of the account's own dead slot still works at red (it creates |
| 146 | no net-new spend). |
| 147 | |
| 148 | ## Messaging (decided 2026-07-14) |
| 149 | |
| 150 | **Aggregate form only.** The canonical line is: |
| 151 | |
| 152 | > **"Paying supports free spaces for those who can't."** |
| 153 | |
| 154 | The gauge publishes the aggregate ("paid spaces currently fund the free pool |
| 155 | MΓ over"), never a per-space "your β¬5 funds these two spaces" β the personal |
| 156 | link isn't mechanically true and invites weird expectations. M is re-derived |
| 157 | with the tier-economics numbers and is verifiable against the public books. |
| 158 | |
| 159 | ## Knobs and placement |
| 160 | |
| 161 | ``` |
| 162 | LCHAT_POOL_MAX_SHARE=25 # X (0/unset = pool off) |
| 163 | LCHAT_POOL_REVENUE_SHARE=30 # Y |
| 164 | LCHAT_POOL_BASE_EUR_DAY=0.20 # floor |
| 165 | LCHAT_POOL_DEMO_SHARE=20 # D |
| 166 | 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" |
| 167 | LCHAT_POOL_DRY_RUN=1 # rollout: compute, log, publish β gate nothing |
| 168 | ``` |
| 169 | |
| 170 | Revenue input: an additive `net_eur_year` field per entry in the existing |
| 171 | `LCHAT_STRIPE_PRICES` JSON (unknown fields are ignored by today's decode; |
| 172 | entries without it contribute 0 and log a warning). With pricing bands |
| 173 | (`docs/pricing-bands.md`), each bandΓtier entry carries its own |
| 174 | `net_eur_year` β discounted spaces grow the pool exactly in proportion to |
| 175 | what they actually pay, which is also the honest version of the marketing |
| 176 | line. Once Stripe Tax ships (tax-inclusive prices), `net_eur_year` figures |
| 177 | are net-of-VAT approximations; the pool tolerates that error comfortably. |
| 178 | |
| 179 | | Layer | Enforces | |
| 180 | |---|---| |
| 181 | | home | nothing pool-related (IP/PoW gates are the demo doc's) | |
| 182 | | cloud | the pool: budget math, pressure, `free_pool_full` admission, demo carve-out + dynamic TTL, `pool_meter`, the reap | |
| 183 | | controld | physical capacity only; the `awake` stat (phase 4). Never learns about money | |
| 184 | | router/activator | hibernation; (phase 4) per-entitlement idle-window override | |
| 185 | |
| 186 | Existing knobs keep their jobs: `LCHAT_CLOUD_MAX_CREATES_PER_DAY` (blunt |
| 187 | spike breaker), `LCHAT_CLOUD_QUOTA`, the physical caps, `LCHAT_POW_BITS`, |
| 188 | the reaper family, storage caps. |
| 189 | |
| 190 | ## Interfaces |
| 191 | |
| 192 | - **Demos** (`docs/demo-spaces.md`): live in the same `spaces` ledger as |
| 193 | `entitlement='demo'`; draw from the carve-out; TTL and live-cap are |
| 194 | pressure-tunable; **never S3-replicate** (decided β the flag is the demo |
| 195 | doc's PR 1). |
| 196 | - **Pricing** (`docs/pricing-bands.md`): the pool consumes one number per |
| 197 | paid space (`net_eur_year`). "Free capacity exists" is defined as |
| 198 | `pressure < 0.8`, exposed read-only (`GET /api/pool`) so the pricing |
| 199 | surfaces and the status page share one source of truth. |
| 200 | |
| 201 | ## Phasing |
| 202 | |
| 203 | 1. **PR 1 β the gauge, no behavior change.** Parse `LCHAT_POOL_*` + |
| 204 | `net_eur_year`; compute spend/budget/pressure in the meter sweep; |
| 205 | `pool_meter` table; log line; admin-token `GET /api/pool`. Ships dark |
| 206 | (pool off unless `LCHAT_POOL_MAX_SHARE` is set). Pure budget-math unit |
| 207 | tests + a seeded meter-sweep test. |
| 208 | 2. **PR 2 β admission control.** `free_pool_full` at `pressure β₯ 1` in |
| 209 | `createSpaceHandler` (free creates only); home relays the message; |
| 210 | `LCHAT_POOL_DRY_RUN` gates. Tests mirror the createBudget ones. |
| 211 | 3. **PR 3 β demo integration.** Carve-out budget, pressure-tuned TTL. |
| 212 | (Sequenced with the demo doc's PRs.) |
| 213 | 4. **PR 4 β measurement.** controld stats gain `awake`; the meter samples a |
| 214 | trailing per-space `awake_frac`; measured beats assumed. Optional |
| 215 | idle-window tightening at amber. |
| 216 | 5. **PR 5 β publish.** The gauge on the status page next to the books |
| 217 | ("the free pool: N spaces, funded MΓ over, state: green"), estimate |
| 218 | caveat included. |
| 219 | |
| 220 | Each PR is independently revertible; nothing before PR 2 can refuse anyone. |
| 221 | The rollout follows the reaper's proven dry-run-then-arm pattern. |
| 222 | |