carlos / platform Public

pricing: annual plans, the affordability index, and the published books

The customer-facing commercial model for Carloku: three annual plans
(Personal, Community, Business) replacing the Hosted rung, Try and the
free pool merged into one pool-funded Free plan, an affordability index
that scales price and inclusions together so margin is constant in every
country, and a books site published as a static app on Carloku itself.

Every allowance is derived from the 80/20 rule over the real rate card
in internal/economics/scenario.go rather than chosen, and every one sits
far beneath its break-even storage. Fleet and Dedicated are untouched.

Records three things that were decided but never written down: the
review promise (price fixed for the term, reviewed annually in public,
tightenings apply to new subscriptions only), the deliberate departure
from Stripe's Metronome recommendation for overage, and the house
discount mechanism for our own accounts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUCJBDGHJoA8PbaTCo4zoD
Paul Campbell pushed by paul@keymail.dev 1dc24945ddcea7885abec90a3b0d862a1954fa3c parent df7d1d2
1 file changed, +445 −0
  • docs/superpowers/specs/2026-09-07-carloku-pricing-plans-design.md +445 −0
diff --git a/docs/superpowers/specs/2026-09-07-carloku-pricing-plans-design.md b/docs/superpowers/specs/2026-09-07-carloku-pricing-plans-design.md
new file mode 100644
index 000000000..284609215
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-07-carloku-pricing-plans-design.md
@@ -0,0 +1,445 @@
+# Carloku pricing — annual plans, the affordability index, and the published books
+
+**Date:** 2026-09-07
+**Status:** Approved direction (Paul, 2026-09-07, in conversation). Every
+number is an anchor that lives in a plans JSON or a scenario, never in code.
+**Grounding:** the live unit-economics instrument
+(`2026-08-11-unit-economics-design.md`, deleted from the tree in the
+open-source redaction pass `2adc71f`, recoverable with `git show`), the
+approved ladder (`2026-08-13-carloku-business-model-design.md`, same), and
+the real rate card in `internal/economics/scenario.go`.
+
+## What this is
+
+The customer-facing commercial model for Carloku: three annual plans, a free
+tier funded by a published budget, an affordability index, and a books site
+that makes every claim on the pricing page checkable. It is the presentation
+layer the 2026-08-13 business-model spec explicitly anticipated:
+
+> Named plans (Heroku-style bundles) are a POSSIBLE FUTURE presentation layer
+> over this model, not part of it.
+
+This spec makes that layer real and, in one place, replaces a rung.
+
+## Relationship to the approved ladder
+
+The five-rung ladder (Try, Free pool, Hosted, Fleet, Dedicated) stays, with
+two changes:
+
+- **Personal / Community / Business replace the Hosted rung.** They are the
+ only way to buy shared multi-tenant capacity. There is no pay-as-you-go
+ Hosted alongside them.
+- **Try and Free pool merge** into one Free plan, funded by the pool.
+
+**Fleet and Dedicated are untouched** by this spec. They remain per-box
+products at the prices the 2026-08-13 spec anchored, and pricing them for
+public sale is separate work.
+
+The one rule survives intact: **you pay per app or per box, never both.**
+Plans bill per member on our boxes. Fleet and Dedicated bill per box and
+carry unlimited apps free. Egress and storage are metered axes everywhere.
+
+## The rule that generates the price book
+
+One rule generates every inclusion:
+
+> **A member's inclusions are the resources whose cost to us is 20% of that
+> member's yearly price. 80% stays for resilience.**
+
+This is Eleven's rule, and it is the reason the allowances are defensible:
+each one is derived, not chosen. Inclusions pool at the account, so a
+ten-person Business account holds one pot of ten members' worth.
+
+Overage past the pooled inclusion is metered at **published input rate × 5**
+(the `Margin: 0.8` already in `DefaultScenario`).
+
+## Cost inputs (EUR, 2026-09)
+
+Converted from the USD anchors in `internal/economics/scenario.go` at 0.92.
+These are the numbers the books page publishes.
+
+| Input | Rate | Source |
+| --- | --- | --- |
+| Box (t4g.small, 2 core / 2 GB) | €0.0169/hour | AWS on-demand eu-west-1 |
+| Object storage | €0.0212/GB-month | S3 standard |
+| Egress | €0.0828/GB | AWS data transfer out |
+| Requests | €0.0046/1k | S3 blended, PUT-heavy |
+| Awake hour (one small app's box share) | **€0.002** | derived, see below |
+| Stripe (EU consumer card) | 1.5% + €0.25 per charge | EUR settlement, no FX |
+
+**The awake-hour anchor is derived from real data, not assumed.** The
+2026-08-15 measurement found an always-on ~190 MB app allocates $1.20–1.80 of
+box per month, which is €0.0015–€0.0023 per hour. €0.002 sits inside that
+range. A hibernating app allocates nothing by metering construction, so it
+costs storage alone — hibernation is the product and the price says so.
+
+## The price book (base, EUR, index 1.0)
+
+| Plan | Price | Members | Inclusions per member, pooled |
+| --- | --- | --- | --- |
+| **Free** | €0 | 1 | 3 apps, 100 awake h/month, 250 MB storage, 1 GB/month egress, 2-minute hibernation, no point-in-time restore |
+| **Personal** | €24/year | 1 | 1,000 awake h/year, 2 GB storage, 2 GB/month egress |
+| **Community** | €5/member/year, 10-member minimum (€50) | unlimited | 200 awake h/year, 0.5 GB storage, 0.4 GB/month egress |
+| **Business** | €50/member/year, 10-member minimum (€500) | unlimited | 2,000 awake h/year, 5 GB storage, 3 GB/month egress, tight durability, priority support |
+
+**A member is a person with console access to the account.** End users of a
+tenant's apps are never counted and never priced. Nothing about a plan limits
+how many people use the apps you deploy.
+
+### The arithmetic, checked
+
+At the ten-member minimum for the team plans:
+
+| Plan | Gross/yr | Net after Stripe | Cost/yr | Margin | Inclusions as % of price | Break-even storage |
+| --- | --- | --- | --- | --- | --- | --- |
+| Personal | €24.00 | €23.39 | €4.50 | 78.7% | 18.7% | 92 GB |
+| Community (10) | €50.00 | €49.00 | €9.24 | 79.5% | 18.5% | 193 GB |
+| Business (10) | €500.00 | €492.25 | €82.50 | 81.9% | 16.5% | 1,939 GB |
+
+**Break-even storage is the number that matters**, and it is Eleven's test:
+the storage a plan could fill forever while still covering itself. Every
+inclusion sits an order of magnitude beneath its break-even, so no plan can
+structurally lose money while still feeling unlimited in practice.
+
+Business margin is deliberately not an infrastructure story. At that margin
+the price buys priority support and features to come, not bytes. The books
+page exists to force that conversation rather than hide it.
+
+### Overage rates
+
+| Meter | Rate | Cost to us |
+| --- | --- | --- |
+| `instance_hours` (awake) | €0.010/hour | €0.002 |
+| `storage_gb_month` | €0.106/GB-month | €0.0212 |
+| `egress_gb` | €0.414/GB | €0.0828 |
+
+**Open knob:** egress at cost × 5 is steep against AWS retail, which a
+customer can price-check in seconds. Unlike compute and storage, egress is
+pure pass-through with no operational value added. A lower multiple (×2–3,
+so €0.17–€0.25) is defensible and still profitable. Recorded as a decision
+for first review against real data, not settled here.
+
+## The affordability index
+
+Prices are indexed to local affordability, using the **World Bank PPP
+conversion factor to market exchange rate ratio** (`PA.NUS.PPPC.RF`) for the
+billing country, banded and clamped.
+
+| Band | Factor | Shape |
+| --- | --- | --- |
+| A | 1.00 | Ireland, EU-15, US, UK, AU, CA |
+| B | 0.75 | |
+| C | 0.50 | |
+| D | 0.35 | floor |
+
+**Price and inclusions scale together.** A band-D member pays 35% of the
+price and receives 35% of the inclusions. This is the direct reading of
+"80/20, but indexed": the rule is what's indexed, not just the number.
+
+The property this buys is the one that matters for a business publishing its
+books: **margin is identical in every country.**
+
+| Band | Personal price | Cost to us | Margin |
+| --- | --- | --- | --- |
+| A (1.00) | €24.00 | €4.50 | 78.7% |
+| B (0.75) | €18.00 | €3.37 | 78.4% |
+| C (0.50) | €12.00 | €2.25 | 77.7% |
+| D (0.35) | €8.40 | €1.57 | 76.8% |
+
+Nobody is subsidised and nobody is penalised. Someone in a band-D country who
+needs more capacity buys it at the same overage rates as everyone else. The
+alternative reading — same product, lower price — was considered and rejected:
+it makes margin fall exactly where volume is likeliest to grow, and it cannot
+be published without inviting the question of who is paying for it.
+
+**Implementation: bands are Prices, not coupons.** A Checkout Session accepts
+only one coupon or promotion code, and that single slot is reserved for the
+customer-facing 50% code. So each band is its own Stripe Price, selected
+server-side from the billing country when the session is created. Four bands
+× three plans = twelve Prices, which is trivial to manage and shows the
+customer an honest price rather than a discount line.
+
+Adaptive Pricing then converts that EUR price into the customer's local
+currency at checkout. It is presentment only — it does not change the price
+level, which is what the index is for. Two constraints, both verified:
+Adaptive Pricing requires the price currency to be a settlement currency
+(EUR is, for `acct_1UD7LXI4Nrr6AEsO`), and it is disabled for any currency
+named in a price's `currency_options`, so we must not set those.
+
+**The index is a knob, reviewed annually, published on the books page.** The
+band assignment for every country is committed data, not a live API call.
+
+## Discounts
+
+Three discount mechanisms, each with a distinct job.
+
+### The 50% no-questions-asked price
+
+A public promotion code, 50% off, `duration: forever`, unlimited redemptions,
+printed on the pricing page. No form, no email, no proof. Renewals keep it.
+
+**Price only — inclusions are not halved.** This is the giveaway, and halving
+what someone receives would defeat its purpose. Margin falls to 55–59%
+depending on band, which is profitable and intentional.
+
+It stacks with the affordability index because the index is a Price and the
+code is the session's one coupon slot.
+
+### The house discount (Paul's own accounts)
+
+A **coupon, not a promotion code**: 100% off, `duration: forever`, attached by
+our backend to accounts carrying a `house` marker in the console store,
+alongside the `tier` column that already exists (`internal/console/store.go`).
+
+A promotion code is customer-facing and can leak; a 100%-forever code in the
+wild is a very bad day. A coupon has nothing to type and reaches only the
+subscriptions we attach it to.
+
+Three consequences that must be built, not assumed:
+
+1. **The house marker must also suppress overage invoicing.** The coupon
+ discounts the subscription invoice; our monthly overage invoices are
+ separate objects we create ourselves and the coupon does not touch them.
+2. **A €0 subscription creates no PaymentIntent.** Entitlement must key off
+ `checkout.session.completed` and the subscription lifecycle events. An
+ integration keyed on payment events would silently never provision exactly
+ these accounts.
+3. **House accounts are a named line in the published books** — real cost
+ against zero revenue, reported separately from the free pool, so
+ "we gave this away to strangers" and "this is the founders' own usage"
+ are never conflated.
+
+### Not a discount: the pool
+
+The free tier is funded, not discounted. See below.
+
+## The free tier and the pool
+
+**One Free plan.** No card, no expiry, every feature. Caps: 3 apps, 100 awake
+hours a month, 250 MB storage, 1 GB a month egress, hibernation at 2 minutes
+idle instead of the 15-minute default (`CARLOS_HIBERNATE_IDLE`).
+
+Aggressive hibernation is the single biggest cost lever, because a sleeping
+app costs storage alone.
+
+**Durability on free: hibernate-flush only.** The platform's baseline promise
+— "hibernate = durable" — still holds, so an app going to sleep is safe. What
+free does not get is continuous replication or restore to an earlier moment.
+Honest to describe as "no backups" without being reckless with data that
+costs us almost nothing to protect.
+
+**Dormancy: archive at 90 days, never delete.** An app nobody has visited for
+90 days has its route removed and its data parked as compressed storage,
+restorable on request. Warning emails at 60 and 80 days. **Paid plans are
+never archived**, which makes "your app is never tidied away" a real reason to
+pay rather than a slogan.
+
+### The pool
+
+ pool = 10% × recognised monthly revenue + bootstrap top-up (€200/mo anchor)
+ draw = Σ free accounts' cost-to-us (the dashboard figure IS the draw)
+ head = pool − draw
+
+Already specified and partly built: the ledger lives at
+`internal/console/economicsview.go:479-580`, `isPoolTier` at `:678`, and the
+top-up accrues per-day against each day's own month length.
+
+**A free account draws about €3.46/year** at full consumption of its caps. The
+€200/month bootstrap top-up alone funds roughly 690 of them before a single
+paying customer contributes.
+
+**As draw approaches the pool, generosity degrades and availability never
+does.** Hibernation tightens, caps step down for new consumption, uploads past
+the storage line are refused. Nothing is killed for budget reasons and nothing
+is deleted. The free tier is therefore self-limiting by construction: there is
+no "the free tier is eating us" cliff, and "how generous can we afford to be"
+is a dashboard line rather than an argument.
+
+## Billing mechanics
+
+### Stripe object model
+
+- **One Product per plan.** Personal, Community and Business are three
+ Products. Never several tiers on one Product: invoice lines and Checkout
+ render the Product name, so sharing one makes them indistinguishable.
+- **Prices**: annual recurring, EUR, one per affordability band per plan.
+ Personal is quantity 1. Community and Business carry the console member
+ count as the subscription quantity, with the 10-member minimum enforced as
+ a floor on quantity.
+- **No `currency_options`** on any price (it disables Adaptive Pricing).
+- **Never pass `payment_method_types`.** Dynamic payment methods are
+ configured in the Dashboard.
+
+Member changes update the subscription quantity. Additions prorate; people who
+leave come off at renewal.
+
+### Checkout and portal
+
+Hosted Checkout Session, `mode: subscription`, `allow_promotion_codes: true`
+for the 50% code, `automatic_tax: { enabled: true }` **once registrations
+exist** — see the tax section. The Customer Portal handles card updates,
+cancellation and invoice history, which is a large surface we then never
+build.
+
+### Entitlement comes from webhooks, never the success page
+
+Handle `checkout.session.completed`, `customer.subscription.created`,
+`.updated`, `.deleted`, `invoice.paid`, `invoice.payment_failed`. Subscription
+state changes happen asynchronously and after checkout, so renewals, failures
+and cancellations are invisible to an integration that reads only the
+redirect.
+
+### What we store
+
+Per account: Stripe customer id, subscription id, plan key, quantity, index
+band, current period end, and the `house` flag. **Nothing else.** A compromise
+of our systems exposes no billing identity — the same property Eleven
+publishes, and it is worth publishing here too.
+
+### Overage: monthly, from our own statements
+
+The hourly economics pass already writes
+`usage/accounts/<acct>/<YYYY-MM>.json` (`internal/usage/statement.go`). A new
+monthly step:
+
+1. reads the closed month's statement,
+2. subtracts pooled inclusions (quantity × per-member inclusion × band factor),
+3. prices the excess at the overage rates,
+4. raises a Stripe invoice with one line per meter **if the total clears a
+ €1 floor**, and waives it below that (chasing €0.40 costs more than €0.40),
+5. skips accounts carrying the `house` flag entirely.
+
+**No Stripe meters, no Metronome.** Stripe's current guidance routes new
+usage-based billing to Metronome, but our statements are already the source of
+truth, computed hourly against real AWS bills. Paying a second metering system
+to recount what we already count would add a reconciliation surface and remove
+none. This is a deliberate departure from the default recommendation and is
+recorded as such.
+
+**Every Stripe write is idempotency-keyed on account + month**, because the
+pass runs hourly and each stage degrades independently — a retry must never
+produce a second invoice.
+
+### Enforcement
+
+Caps enforcement was deferred by the 2026-08-13 spec and is still unbuilt. It
+reads the plan from the store and drives: per-app hibernation idle, app count,
+storage refusal, and the pool's degradation steps. It is a prerequisite for
+the free tier being safe to offer and belongs in the implementation plan, not
+a later spec.
+
+## Tax
+
+**Stripe Tax with `automatic_tax` collects nothing, and reports no error,
+until a registration shows as Collecting.** This is the single most common
+Stripe Tax mistake and it fails silently.
+
+State of `acct_1UD7LXI4Nrr6AEsO` as of 2026-09-07:
+
+- Head office set (64 Dame Street, Dublin), settings status `active`. ✅
+- **Tax registrations: none.** ❌
+- Default tax code is `txcd_10000000` (General - Electronically Supplied
+ Services), which the guidance warns is too broad. Hosting is SaaS
+ (`txcd_10103001` is the likely correct code) — **an accountant's call, not
+ ours.**
+
+Required before live: Irish VAT registration with Revenue, EU OSS for
+cross-border B2C, both recorded in Stripe, and the product tax code confirmed.
+
+## The published books
+
+Three static pages, shipped as a **static Carlos app on Carloku**. The
+platform's own books run on the platform, which is the dogfooding point and
+the reason this is not a separate status host.
+
+The mechanism exists: `carlos ship --kind static` packs a directory
+(`internal/edge/static.go`), static routes carry no instance records, and a
+console pass can materialise `economics/public/*.json` into the deployment
+bucket for the pages to read.
+
+| Page | Content | Data source |
+| --- | --- | --- |
+| Income & expenditure | what came in, what went out | expenditure: **real today** from `economics/bills/<month>.json` (Cost Explorer). Income: **does not exist yet** — nothing takes payment. |
+| Unit economics | input rates, the 80/20 rule, per-plan margin and break-even | statements × active scenario |
+| The pool | budget, draw, headroom, house accounts on their own line | `internal/console/economicsview.go` |
+
+**The honesty rule, which is the entire value of publishing:** never put a
+modelled number in a slot a reader will take as actual. Until Stripe is live,
+the income page says income does not exist yet rather than rendering modelled
+revenue. Estimates are labelled estimates. Following Eleven: *the formulas are
+the contract; the numbers are re-derived whenever prices or input rates
+change.*
+
+## The review promise
+
+The 2026-08-13 spec committed only to re-deriving numbers whenever inputs
+move. That is weaker than a customer needs. This spec makes it explicit:
+
+1. **Your price is fixed for the year you bought.** An increase never reaches
+ a subscription mid-term.
+2. **Rates and the affordability index are reviewed once a year, in public**,
+ on the books page, with the previous snapshot kept.
+3. **Tightenings apply to new subscriptions only.** Existing accounts keep the
+ deal they signed up for until they choose to change it.
+
+Point 3 costs almost nothing to honour and is the most trust-building sentence
+available on a pricing page. It is Eleven's "constraints ratchet only for new
+spaces" applied here.
+
+## What exists vs what this needs
+
+**Exists and is live:**
+
+- Frozen meter vocabulary (`internal/usage/usage.go:16-42`) — `instance_hours`,
+ `storage_gb_month`, `egress_gb` are exactly the three axes this spec prices.
+- Pricing arithmetic and free-tier subtraction (`internal/economics/compute.go`).
+ **Note the warning at `compute.go:95`:** these formulas are mirrored verbatim
+ by inline JS in `internal/console/templates/economics.html` and must change
+ together.
+- Monthly per-account statements, storage sweep, census, AWS bill pull
+ (`internal/console/economicspass.go`).
+- Account tier column and operator door (`internal/console/store.go:839-869`).
+- The pool ledger (`internal/console/economicsview.go:479-580`).
+- Hibernation (`internal/activator/activator.go`), and the scheduled-tick
+ ruling that residency is measured from each **wake**, not each tick
+ (`2026-08-30-scheduled-ticks-and-hibernation-design.md` §3.1–3.2) — the
+ billing arithmetic must keep that convention.
+- Static app kind, for the books site.
+
+**Needs building:**
+
+1. Plans JSON (plan keys, inclusions, band factors, country→band map) as
+ committed data with a schema, beside the scenarios.
+2. Stripe catalogue: 3 Products, 12 Prices, 2 coupons, 1 promotion code.
+3. `house` marker on accounts + console door.
+4. Checkout + portal doors on the console; band selection from billing country.
+5. Webhook endpoint and entitlement writes.
+6. Monthly overage pass with idempotency keys and the €1 floor.
+7. Caps enforcement (hibernation idle per plan, app count, storage refusal,
+ pool degradation steps).
+8. Dormancy archive at 90 days with 60/80-day warnings.
+9. Books site: publisher pass + three static pages.
+10. `CARLOS_STRIPE_SECRET_KEY` and `CARLOS_STRIPE_WEBHOOK_SECRET` read in
+ `cmd/console/main.go` `serve()`, supplied from the pinfra EnvironmentFile
+ exactly as `CARLOS_RELEASE_KEY` is. **A restricted key (`rk_`), not a
+ secret key.** Never in the deployment bucket, never in `apps/*/config.json`.
+
+## Open questions and follow-ups
+
+- **The egress multiple** (see Overage rates) — settle at first review against
+ real data.
+- **Fleet and Dedicated public pricing** — untouched here, needs its own pass
+ before those rungs can be sold rather than negotiated.
+- **Discussion #82**, backhauled egress: still open, still awaiting a pricing
+ call. Unchanged by this spec; the safe default remains absorbed.
+- **The mail tier vocabulary** (`internal/console/mailcounts.go:68` — `pro`,
+ `starter`, default) does not match these plan names and will need
+ reconciling before the two are visible in the same interface.
+- **Noisy neighbours** (discussion #1: one tenant took a whole box and nothing
+ noticed) — a per-plan memory ceiling is the obvious lever and is not in this
+ spec. Enforcement work should not land without it.
+- **Pricing page copy** — follows this spec, and goes through the copy-review
+ gate before it is written anywhere.
+- **Control-plane polling cost** (~$1/box/month of 2-second heartbeat GETs) is
+ the platform's own floor and shows up in the published expenditure. Worth a
+ look before the fleet is 50 boxes.