docs: unit-economics design spec (metering, cost dashboards, scenarios)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+234
−0
docs/superpowers/specs/2026-08-11-unit-economics-design.md+234 −0
diff --git a/docs/superpowers/specs/2026-08-11-unit-economics-design.md b/docs/superpowers/specs/2026-08-11-unit-economics-design.md| new file mode 100644 |
| index 000000000..b1415dcbf |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-08-11-unit-economics-design.md |
| @@ -0,0 +1,234 @@ |
| +# Unit economics — metering, cost dashboards, and business-model scenarios |
| + |
| +**Date:** 2026-08-11 |
| +**Status:** Approved design (Paul, 2026-08-11) |
| +**Scope:** Generic per-deployment platform feature, operator-gated. On the |
| +flagship console it models carloku.com's economics; on tito it models |
| +Eleven's own costs. |
| + |
| +## What this is |
| + |
| +A **modelling instrument**, not a billing system. Real metered usage and |
| +real AWS costs feed an admin-only what-if dashboard with tweakable prices, |
| +margins and free-tier knobs. Numbers are decision-grade, not invoice-grade: |
| +gaps and re-derivation are tolerated, and every figure states its coverage. |
| +Invoice-grade metering is explicitly out of scope until the business model |
| +settles. |
| + |
| +Questions it must answer: |
| + |
| +- What does each box cost us, and what profit does it produce? |
| +- What does our AWS spend look like per service, and does the model match |
| + the actual bill? |
| +- What does each account cost us, and what would it earn at a chosen |
| + margin or rate sheet? |
| +- What would a candidate free tier cost, and who lives inside it? |
| + |
| +## Decisions taken |
| + |
| +| Decision | Choice | |
| +| --- | --- | |
| +| Purpose | Modelling instrument (decision-grade) | |
| +| Cost truth | Tweakable rate card × metered usage, reconciled monthly against the real AWS bill | |
| +| Meters v1 | CPU + memory, edge requests + egress bytes, storage bytes, instance-hours / census | |
| +| Knobs | Live client-side recompute + named scenarios persisted to bucket config | |
| +| Scope | Generic, per-deployment, operator-gated | |
| +| Architecture | Extend the existing metrics pipeline (60s samples → hourly chunks → daily rollups → console sweep + cache); no new ledger, no new daemon | |
| + |
| +## Collection & data model |
| + |
| +Three new object families under `usage/` in the deployment bucket. All |
| +additive: no existing wire shape changes, and the only parser of the new |
| +shapes is the console. |
| + |
| +### Edge requests + egress (new, box-side — the only new box collector) |
| + |
| +- `internal/edge` gains per-route atomic counters: request count and |
| + response bytes out, incremented via a wrapped `ResponseWriter` in the |
| + serving path. Atomic adds only; no per-request allocation. |
| +- Flushed every 5 minutes into hourly chunks |
| + `usage/edge/<box>/<hour>.json`, rows |
| + `{account, app, host, requests, bytesOut}` — same chunk discipline as |
| + `metrics/boxes/<box>/raw/`. |
| +- The edge process rolls closed hours into `usage/edge/<box>/<day>.json`, |
| + mirroring `metrics.RollupHour`. Raw hourly chunks pruned after 48h. |
| +- **Caveat (recorded, accepted):** this meters what passes the edge |
| + router. Tunnel traffic and non-edge-fronted paths are unmetered in v1. |
| + |
| +### Storage bytes (new, console-side daily pass) |
| + |
| +- A daily tick in the console sizes bucket contents by prefix — releases, |
| + app logs, config — attributed per app/account, plus tenant object-store |
| + buckets via their stored credentials. |
| +- Written to `usage/storage/<day>.json`, guarded by conditional PUT |
| + (control-plane buckets guarantee it) so a restarted or duplicate console |
| + cannot double-write. Content is deterministic per day. |
| + |
| +### Instance-hours + census (derived, console-side daily pass) |
| + |
| +- No new box collection: a unit appearing in an existing 60s metrics |
| + sample **is** one minute of running time; running instance-hours are |
| + derived from metrics chunks already in the bucket. |
| +- The daily tick snapshots per-account counts — apps, routes, running vs |
| + hibernated instances — into `usage/census/<day>.json`, giving |
| + existence-metering history even though the registry holds only live |
| + state. |
| + |
| +### CPU / memory GB-hours (existing) |
| + |
| +- Rolled up unit → app → account at read time from the metrics rollups |
| + already in the bucket (`internal/metrics`). No collection changes. |
| + |
| +## Economics model |
| + |
| +### Rate card — what things cost us |
| + |
| +Per-deployment JSON of unit costs: |
| + |
| +- Per-box EC2 hourly rate, with per-box overrides (flagship and tito boxes |
| + differ). |
| +- EBS GB-month, S3 storage GB-month, S3 requests per-1k, egress per-GB. |
| +- A free-form list of fixed monthly overheads (domains, DNSimple, anything |
| + not usage-shaped). |
| + |
| +Defaults seeded from real current pricing (tito boxes run ~$55–65/mo |
| +all-in; flagship seeded with its own numbers). |
| + |
| +### Cost attribution |
| + |
| +- **Shared costs** (EC2 + EBS per box) are allocated to apps by resource |
| + share. The allocation policy is a knob: by CPU-hours, by memory |
| + GB-hours, or a weighted blend. |
| +- **Idle headroom is never smeared across tenants.** Unallocated capacity |
| + shows as a "platform overhead / idle" line per box — per-box utilization |
| + is exactly what the profit-per-box view exists to expose. |
| +- **Direct-metered costs** (egress, storage, S3 requests) attach to |
| + accounts with no allocation step. |
| + |
| +### Pricing — what we'd charge |
| + |
| +Two modes, switchable per scenario: |
| + |
| +- **Cost-plus:** price = cost ÷ (1 − margin); margin knob defaults to 0.80. |
| + Margin is defined as (price − cost) / price — so 80% margin means a |
| + **5×** multiple on cost. The UI shows both margin and multiple, always. |
| +- **Rate sheet:** explicit customer prices (per app/month, per GB egress, |
| + per GB-month storage, per running-instance-hour); the dashboard shows |
| + the margin those prices *achieve* per account given real usage. |
| + |
| +### Free tier — a subtraction, not a flag |
| + |
| +Per-account allowances (N apps, X GB egress, Y GB-hours, Z storage GB) are |
| +subtracted before revenue is computed. The dashboard answers: how many |
| +accounts live entirely inside the free tier, what the free tier costs per |
| +month, and what revenue it forgoes. |
| + |
| +### Scenarios |
| + |
| +A scenario = rate card + allocation policy + pricing mode + prices/margin |
| ++ free-tier allowances. |
| + |
| +- Stored at `economics/scenarios/<name>.json`; `economics/active.json` |
| + names the default. Versioned manifests in the `internal/appconfig` |
| + style. |
| +- Knob changes recompute in-page instantly (client-side, over embedded |
| + usage JSON). "Save scenario" persists via an operator-gated, |
| + CSRF-protected console endpoint. |
| + |
| +### Bill reconcile |
| + |
| +- `economics/bills/<YYYY-MM>.json`: real monthly AWS bill, total plus |
| + by-service, `source: manual` in v1. |
| +- Entered through a small console form **and** a `carlos economics bill` |
| + operator verb (the no-box-commands rule holds either way). |
| +- Dashboard renders model-vs-actual delta per service, so rate-card drift |
| + is visible the month it happens. |
| +- Automated Cost Explorer ingestion is a filed follow-up, not v1: it needs |
| + new IAM and tagging discipline, and both AWS accounts hold non-CARLOS |
| + resources (keymail's on flagship, Tito's legacy fleet on titogo), so an |
| + unfiltered bill overstates CARLOS costs. |
| + |
| +## Console surface |
| + |
| +Both pages gated identically to `/fleet`: session → `IsOperator` → bare |
| +`http.NotFound`. Route existence is never probeable. |
| + |
| +### `/fleet/economics` — the business dashboard |
| + |
| +- Headline tiles: would-be revenue, cost, profit, achieved margin for the |
| + period. |
| +- Per-box table: box, instance class, cost/month, utilization, revenue |
| + attributable to its tenants, **profit per box**. |
| +- Per-service AWS cost breakdown (EC2, EBS, S3 storage, S3 requests, |
| + egress, overheads) side-by-side with the imported bill and the delta. |
| + |
| +### `/fleet/economics/accounts` — every account |
| + |
| +- Columns: apps, instances, CPU-hours, GB-hours, egress, storage, **cost |
| + to us**, would-be revenue under the active scenario, margin, free-tier |
| + position (inside / partially over / paying). |
| +- Search + cursor pagination reusing the `listquery` machinery from the |
| + instances page. |
| + |
| +### Shared behaviour |
| + |
| +- Both pages embed aggregated usage as JSON; the knob drawer (rate card, |
| + allocation, margin, prices, free tier) recomputes every number |
| + client-side with no round-trip. |
| +- Period selector: month-to-date with projection, last month, 7 days. |
| +- Aggregation happens console-side at render over daily rollups, behind |
| + the same 60s-TTL stale-while-revalidate cache pattern as `LoadMetrics`. |
| + |
| +### Honesty rules (dashboard-truth discipline) |
| + |
| +- Every total states its coverage: "23/30 days metered". |
| +- New meters start with zero history and must say so, never render as |
| + cheap. |
| +- A box running a pre-counter edge binary shows egress **unmetered**, |
| + never zero. |
| +- Projections are labelled projections. |
| + |
| +### CLI |
| + |
| +v1 viewing is console-only. `carlos economics bill` (operator verb) ships |
| +in v1 for bill entry; a read-side `carlos economics` summary verb is filed |
| +as a follow-up gap rather than built now. |
| + |
| +## Error handling |
| + |
| +- Missing chunk/rollup days render as gaps and reduce the stated |
| + coverage; they never silently zero a total. |
| +- Conditional-PUT conflicts on daily passes are treated as "someone else |
| + wrote today" and skipped. |
| +- Scenario save validates shape server-side; malformed active scenario |
| + falls back to built-in defaults with a visible warning banner. |
| +- Object-store credential failures during the storage sweep mark that |
| + store "unswept" for the day rather than aborting the pass. |
| + |
| +## Testing |
| + |
| +- **Golden-number unit tests for the maths**: allocation policies, |
| + margin-vs-markup, free-tier subtraction, projection. These are exactly |
| + the bugs that would quietly mislead a business decision. |
| +- Concurrent-increment and flush tests for edge counters. |
| +- Rollup and conditional-PUT-guard tests for the daily passes. |
| +- Operator-gating tests asserting the byte-identical 404 on both pages |
| + and the scenario/bill endpoints. |
| +- Standard gate: `go build ./...`, `go vet ./...`, `go test ./... |
| + -count=1`, `gofmt -l` clean, GOOS=darwin compile. |
| + |
| +## Rollout |
| + |
| +- All additive. Boxes need the new edge binary (ship → promote → |
| + converge) before egress data accrues; console deploys with the usual |
| + restart rule. |
| +- The dashboard is useful on day one from existing CPU/mem history; |
| + edge, storage and census series fill in as they accrue. |
| + |
| +## Out of scope (v1) |
| + |
| +- Payments, invoicing, or any customer-facing pricing display. |
| +- Invoice-grade usage ledger (revisit when payments arrive). |
| +- Automated Cost Explorer ingestion (filed follow-up). |
| +- Tunnel/non-edge traffic metering. |
| +- Read-side `carlos economics` CLI verb (filed follow-up). |