carlos / platform Public

pricing: revise the spec against all fourteen codex findings

The commercial model survives; the billing mechanism did not. Six
metering prerequisites now gate the first invoice: statement closure
with a late-data policy, ownership stamped at collection time,
invoice-grade collection replacing the any-source covered-days rule,
new store_egress_gb and durability_writes_1k meters, and durable
per-account-month billing records instead of bare idempotency keys.

Inclusions become monthly, because an annual allowance cannot be
subtracted from a monthly statement without a carried balance or a
wrong answer (Paul: monthly, no rollover). Object-store egress gets
metered rather than declared free, and the reserved durability meter
gets built, both on Paul's ruling; tight durability ships with a
bounded 50k-write inclusion because unbounded it goes loss-making near
700k writes per member per month.

Two arithmetic corrections the reviewer caught: Stripe Billing's 0.7%
was omitted, and the break-even storage figures spent revenue the other
inclusions had already claimed, overstating them by about 20%. Terms
are now versioned per subscription so the review promise is actually
enforceable, and the pool must be funded from recognised revenue rather
than modelled revenue before the books are published.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 4d545db38dec07c12f4014ec078809a234b3d4a8 parent ae68c79
1 file changed, +251 −61
  • docs/superpowers/specs/2026-09-07-carloku-pricing-plans-design.md +251 −61
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
index 284609215..e2d49e230 100644
--- a/docs/superpowers/specs/2026-09-07-carloku-pricing-plans-design.md
+++ b/docs/superpowers/specs/2026-09-07-carloku-pricing-plans-design.md
@@ -1,8 +1,15 @@
# 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.
+**Status:** Approved direction (Paul, 2026-09-07, in conversation), revised
+after an independent review by GPT-6 via codex. Every number is an anchor
+that lives in a plans JSON or a scenario, never in code.
+**Second reader:** `CODEX-FINDINGS-carloku-pricing.md`, committed unedited
+against `1dc2494`. Fourteen findings, seven P1, all seven code citations
+independently verified. Accepted findings are marked **(codex N)** below.
+Its verdict on the first draft was that the commercial model was sound and
+**the billing mechanism was not ready to implement** — the revision is mostly
+about that gap.
**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
@@ -63,9 +70,14 @@ These are the numbers the books page publishes.
| 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 |
+| S3 requests | €0.0046/1k | 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 |
+| Stripe | **1.5% card + 0.7% Billing + €0.25** per charge | Stripe Ireland pricing, EUR settlement, no FX |
+
+Stripe Billing's 0.7% applies to every recurring charge and the earlier draft
+omitted it (codex 12). Stripe Tax adds a further 0.5% on transactions in
+registered jurisdictions once registrations exist; that is not yet modelled
+and must be added at the first review, when the registrations are real.
**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
@@ -77,10 +89,18 @@ costs storage alone — hibernation is the product and the price says so.
| 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 |
+| **Free** | €0 | 1 | 3 apps, 100 awake h, 250 MB storage, 1 GB egress, 2-minute hibernation, no point-in-time restore |
+| **Personal** | €24/year | 1 | 85 awake h, 2 GB storage, 2 GB egress |
+| **Community** | €5/member/year, 10-member minimum (€50) | unlimited | 17 awake h, 0.5 GB storage, 0.4 GB egress |
+| **Business** | €50/member/year, 10-member minimum (€500) | unlimited | 170 awake h, 5 GB storage, 3 GB egress, 50k replication writes, priority support |
+
+**All inclusions are per member per MONTH** (codex 3). They pool at the
+account and are subtracted from that month's statement, so the allowance
+period and the billing period are the same window. An annual allowance
+cannot be subtracted from a monthly statement without either a carried
+balance or a wrong answer: 600 awake hours in each of two months blows a
+1,000-hour year while showing no monthly excess. Ruling (Paul, 2026-09-07):
+monthly inclusions, no annual balance, no rollover.
**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
@@ -88,30 +108,40 @@ how many people use the apps you deploy.
### The arithmetic, checked
-At the ten-member minimum for the team plans:
+At the ten-member minimum for the team plans. Stripe is 1.5% card + **0.7%
+Billing** + €0.25 (codex 12 — the earlier draft omitted the Billing fee).
| 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 |
+| Personal | €24.00 | €23.22 | €4.54 | 77.9% | 18.9% | 76 GB |
+| Community (10) | €50.00 | €48.65 | €9.32 | 78.7% | 18.6% | 160 GB |
+| Business (10) | €500.00 | €488.75 | €110.90 | 75.6% | 22.2% | 1,538 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.
+the storage a plan could fill forever while still covering itself. These are
+**residual** break-evens — the other inclusions are consumed first, then the
+remaining net buys storage (codex 14). The earlier draft divided net by the
+storage rate alone and so overstated every figure by about 20%. Publishing an
+inflated break-even on a page whose entire purpose is checkability would have
+been the worst possible first impression.
+
+Every inclusion still sits an order of magnitude beneath its break-even, so
+no plan can structurally lose money while 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.
+**Inclusions are 18.6–22.2% of price, not exactly 20%** (codex 13). The rule
+sizes them; round numbers a human can remember win the last percent. The
+rule's claim is the bound — inclusions never exceed a fifth of the price —
+not an exact equality.
### 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 |
+| Meter | Rate | Cost to us | Multiple |
+| --- | --- | --- | --- |
+| `instance_hours` (awake) | €0.010/hour | €0.002 | ×5 |
+| `storage_gb_month` | €0.106/GB-month | €0.0212 | ×5 |
+| `egress_gb` | €0.414/GB | €0.0828 | ×5 |
+| `store_egress_gb` **(new)** | €0.414/GB | €0.0828 | ×5 |
+| `durability_writes_1k` **(new)** | €0.023/1k | €0.0046 | ×5 |
**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
@@ -119,6 +149,35 @@ 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.
+### Why tight durability carries a bounded inclusion (codex 6)
+
+Business includes tight durability, and the replication-write meter the
+2026-08-13 spec reserved as `durability_writes_1k` **must be built as part of
+this work** (ruling, Paul 2026-09-07). Sizing it is not optional:
+
+| Replication writes /member/month | Business cost/yr | Margin |
+| --- | --- | --- |
+| 0 | €83.30 | 81.1% |
+| 50k **(included)** | €110.90 | 75.6% |
+| 100k | €138.50 | 70.0% |
+| 500k | €359.30 | 25.9% |
+| ~700k | — | break-even |
+| 1M | €635.30 | **−29.3%** |
+
+An unbounded "tight durability" inclusion goes loss-making at roughly 700k
+writes per member per month. So Business includes **50k writes per member per
+month** and meters beyond it at €0.023/1k. A continuous writer at 1M/month
+pays about €2,760/year against €552 of cost — fair by construction, since you
+pay for the durability you actually consumed.
+
+**Do not reuse `requests_1k` for this** (codex 5). That meter is fed
+exclusively from edge HTTP request counts
+(`internal/console/economicspass.go:407`) yet priced at an S3 request rate
+(`compute.go:102`, default €0.0046 from `scenario.go:145`, and the console
+even labels it `$s3requests`). That mismatch is a live bug in the modelling
+instrument, not just a spec problem, and should be fixed or renamed
+independently of this work.
+
## The affordability index
Prices are indexed to local affordability, using the **World Bank PPP
@@ -141,10 +200,17 @@ 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% |
+| A (1.00) | €24.00 | €4.54 | 77.9% |
+| B (0.75) | €18.00 | €3.40 | 77.5% |
+| C (0.50) | €12.00 | €2.27 | 76.8% |
+| D (0.35) | €8.40 | €1.59 | 75.9% |
+
+**Margin drifts about two points across the bands, it is not flat** (codex
+13). Infrastructure cost scales exactly with the band, but Stripe's fixed
+€0.25 does not, so it weighs proportionally more on a smaller price. The
+invariant is the resource-cost ratio; margin after fees is close to constant,
+not constant. The books page says so rather than claiming a precision it
+does not have.
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
@@ -228,6 +294,13 @@ 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.
+**This needs a new replication class (codex 8).** The existing classes are
+stream, five-minute and daily, and the *unset* value means stream
+(`internal/activator/activator.go:209`), so a free app left alone would
+replicate continuously — the opposite of the intent. Flush-only must be added
+as a class and propagated from the account's plan. Shortening the hibernation
+idle window alone does not achieve it.
+
**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
@@ -244,6 +317,17 @@ 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.
+**"Recognised revenue" must become real revenue before this is published
+(codex 10).** The existing ledger funds the pool from `PriceAccount`
+(`economicsview.go:523`, used at `:567`), which is *modelled* revenue derived
+from meters. Under this spec that is wrong in both directions: a paid but
+idle annual subscriber generates almost no modelled revenue while having
+actually paid, and a heavy free-tier user generates modelled revenue while
+paying nothing. Once Stripe is live, the pool must be funded from annual
+subscription income recognised across its service period, net of discounts
+and refunds. Publishing the current ledger unchanged would break this spec's
+own honesty rule on the very page that asserts it.
+
**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.
@@ -291,34 +375,105 @@ 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.
+Per account: Stripe customer id, subscription id, plan key, **terms version**,
+quantity, index band, current period end, and the `house` flag. Plus the
+per-account-month billing records described above. **No billing identity** —
+no name, no email, no card details. A compromise of our systems exposes none
+of it, the same property Eleven publishes, and 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.
+`usage/accounts/<acct>/<YYYY-MM>.json` (`internal/usage/statement.go`). This
+is the highest-risk surface in the spec: an hourly pass, a monthly boundary,
+and a payment processor. **Six prerequisites must land before a single
+invoice is raised**, every one of them found by the second reader and
+verified against the code.
+
+**P1. Statements must actually close (codex 2).** `statementsPass` derives
+its month from `now` (`economicspass.go:751-754`) and never revisits a closed
+one, while host metrics only roll up an hour *after* it ends
+(`metrics_loop.go:179-208`). September's final hours therefore land in
+October, when nothing is updating September any more. Waiting longer does not
+fix it. Needs an explicit previous-month catch-up stage and a defined
+late-data policy, on the model of `billPass`, which already reaches back one
+month (`economicspass.go:859-860`).
+
+**P2. Ownership must be captured at collection time (codex 4).** The sweep
+re-attributes *every day of the month* against the **live** registry through
+`unitOwner`, and an unmatched unit falls to `release.OpsAccount`
+(`economicspass.go:211-215`). Retired routes are hard-deleted, not
+tombstoned (`internal/provision/reconcile.go:1005`). So an account that
+deletes an app mid-month silently stops being billed for the hours it already
+used. Ownership must be stamped when usage is recorded, or held as
+effective-dated history. Closing the statement does not fix this.
+
+**P3. Collection must be invoice-grade before it is invoiced (codex 5).**
+`usageFlush` passes `drain()` as an argument, so counters are emptied whether
+or not the write succeeds, and the caller only logs the failure — the doc
+comment at `usage_loop.go:51` concedes the window is lost. Worse for billing,
+`CoveredDays` marks a day covered if metrics *or* edge *or* storage produced
+anything (`economicspass.go:341-432`), so a storage sweep can mask an entire
+day of missing egress while the statement still reports full coverage. The
+2026-08-11 spec was explicit that this instrument is "decision-grade, not
+invoice-grade"; billing it without a per-meter completeness policy would
+charge customers for numbers we know can be silently short.
+
+**P4. Idempotency needs a durable record, not just a key (codex 1).** Stripe
+prunes idempotency keys after 24 hours, so an account+month key reused by an
+hourly pass a day later creates a *new* request rather than replaying the old
+one. Needs a persisted per-account-month billing record holding the
+calculation snapshot, the Stripe object ids and a completion state, with
+operation-specific keys inside it.
+
+**P5. Object-store egress must be metered (codex 7, ruling Paul
+2026-09-07).** Apps receive bucket coordinates *and* credentials
+(`internal/objstore/walk.go:591`) with direct `s3:GetObject`
+(`internal/objstore/aws.go:180`), while `egress_gb` is fed only from edge rows
+(`economicspass.go:406`). Bytes served or presigned straight from the bucket
+are invisible to the meter today. A new `store_egress_gb` meter attributes S3
+data transfer per tenant and shares the egress allowance. Until it exists,
+the advertised egress allowance is not the whole story and the pricing page
+must not claim otherwise.
+
+**P6. Replication writes must be metered (codex 6).** See the durability
+section above: `durability_writes_1k` is a new meter, not a reuse of
+`requests_1k`.
+
+Only then does the monthly step run:
+
+1. read the **closed** month's statement, after the catch-up stage has
+ declared it final,
+2. subtract pooled inclusions (quantity × per-member monthly inclusion ×
+ band factor), using **the terms version the account holds**, not today's
+ plans file (see below),
+3. price the excess at that terms version's overage rates,
+4. raise a Stripe invoice with one line per meter **if the total clears a
+ €1 floor**, waiving below that (chasing €0.40 costs more than €0.40),
+5. skip accounts carrying the `house` flag entirely,
+6. record completion in the billing record before returning.
+
+**Terms are versioned, not just named (codex 9).** A plan key plus a band
+identifies a *mutable* definition, so editing the plans file would silently
+re-price existing accounts and break the review promise below. Scenario names
+have the same weakness — the writer replaces the named object in place
+(`economicspass.go:1020`). Each subscription therefore pins an immutable
+terms version covering allowances, overage rates and band factor. The review
+promise is only enforceable if the terms an account accepted are still
+readable a year later.
**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.
+recorded as such. It does mean the six prerequisites above are ours to build
+rather than bought, which is the honest cost of the choice.
-**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.
+**Quantity proration is a choice, not a default.** Adding a member prorates,
+but Stripe does not automatically collect a positive proration immediately —
+the implementation must decide whether it lands on the next invoice or is
+charged at once, and say which on the pricing page.
### Enforcement
@@ -353,9 +508,20 @@ 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.
+(`internal/edge/static.go`) and the edge serves it as a first-class app kind.
+
+**Two corrections to the first draft (codex 11).** Writing
+`economics/public/*.json` into the deployment bucket would *not* make it
+readable by the site: the static handler serves an extracted release tree and
+joins request paths onto a local directory (`static.go:143`, `serveTree` at
+`:364-378`), never a bucket key. So the books are published one of two ways,
+and the plan must pick: either the publisher pass ships a **new static
+release** on each update, or the console exposes an explicit public data
+endpoint the pages fetch. A release per update is the more honest fit — the
+books then carry a version and a date like any other deployment.
+
+And static routes **do** carry provision records (`provision.go:58-61`,
+accepted at `:358`, branched at `:434`). The first draft claimed otherwise.
| Page | Content | Data source |
| --- | --- | --- |
@@ -406,24 +572,48 @@ spaces" applied here.
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
+**Needs building.** Steps 1–6 are the metering prerequisites and **must land
+before any invoice is raised**; they are the bulk of the work and the reason
+this spec is not a small one.
+
+1. Previous-month catch-up and statement closure, with a late-data policy
+ (codex 2).
+2. Ownership stamped at collection time, or effective-dated ownership history
+ (codex 4).
+3. Invoice-grade collection: durable usage writes and a per-meter
+ completeness policy replacing the any-source `CoveredDays` (codex 5).
+4. `store_egress_gb` meter — per-tenant S3 data-transfer attribution
+ (codex 7).
+5. `durability_writes_1k` meter — per-tenant replication-write attribution,
+ plus a flush-only replication class and plan propagation (codex 6, 8).
+6. Per-account-month billing records with operation-scoped idempotency
+ (codex 1).
+7. Plans JSON with **immutable terms versions** (plan keys, monthly
+ inclusions, overage rates, band factors, country→band map) as committed
+ data with a schema, beside the scenarios (codex 9).
+8. Stripe catalogue: 3 Products, 12 Prices, 2 coupons, 1 promotion code.
+9. `house` marker on accounts + console door.
+10. Checkout + portal doors on the console; band selection from billing
+ country; an explicit proration-collection choice.
+11. Webhook endpoint and entitlement writes.
+12. Monthly overage pass with the €1 floor.
+13. Caps enforcement (hibernation idle per plan, app count, storage refusal,
+ pool degradation steps).
+14. Dormancy archive at 90 days with 60/80-day warnings.
+15. Pool funded from recognised subscription revenue rather than modelled
+ revenue (codex 10).
+16. Books site: publisher pass shipping a static release + three pages
+ (codex 11).
+17. `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`.
+**Sequencing.** 1–6 are independently useful — they improve the modelling
+instrument whether or not anyone ever pays — and can land while the Stripe
+account's tax registrations are still pending. 7–12 are the commercial layer.
+13–14 gate the free tier being safe to offer. 15–16 gate publishing the books.
+
## Open questions and follow-ups
- **The egress multiple** (see Overage rates) — settle at first review against