carlos / platform Public

docs: unit-economics design spec (metering, cost dashboards, scenarios)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 091de05dda9fd62ca8c918f7f018818d6f8e9b14 parent a73539f
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).