18 KiB
PR7 Recon — DESIGN_POV #4: BYO-key vs Waggle-metered (the inference-cost decision)
RECON ONLY. No product code changed. Every claim below is grounded with
file:line. Topic: make the founder's BYO-vs-metered choice concrete with build cost, grounded in what already shipped through PR1–PR6. PR7 = screen 13 Auth (Clerk) + screen 14 Billing (Stripe) perdocs/redesign-warm-hive/BUILD-PLAN.md §6(line 145).
0. TL;DR (the de-facto commitment)
The codebase has already committed to Option A: BYO-key + flat SUBSCRIPTION tiers, with no inference metering. This isn't a design intention — it is shipped, wired, and tested across three surfaces:
- BYO-key is the only inference-payment path that exists. Onboarding's hard model gate and Settings → Models both mount the same
ModelGatewhose entire copy is "Bring your own key — it's stored encrypted in your Vault and never leaves your machine." (apps/web/src/components/os/model-gate/ModelGate.tsx:186). Waggle never holds an inference key or pays a provider on the user's behalf in any shipped path. - Stripe billing is flat
mode: 'subscription'— both the desktop sidecar (packages/server/src/stripe/checkout.ts:40) and theapps/wwwcloud port (apps/www/app/api/stripe/checkout/route.ts:170). Two products (Pro $19, Teams $49/seat), monthly/annual, period-end renewal. Nomode: 'payment', no usage records, no metered prices. - There is zero inference-metering plumbing. No
createUsageRecord, nobilling_meter, noreportUsage, no credits/balance ledger anywhere inpackages/server/src(verified by grep — only hit is a comment about not burning Anthropic credits inpackages/server/src/local/index.ts:1980). The only "usage" surface is an estimate-based read-only cost dashboard (/api/cost/summary) with a soft, advisory daily-budget warning — never a hard cap, never tied to billing.
Recommendation: ratify Option A. PR7 Billing becomes a theming task over an already-working subscription flow (near-zero new backend). Option B (Waggle-metered) is a multi-month strategic pivot touching billing, onboarding, the inference path, quota enforcement, and a new credits surface — and it contradicts the local-first / "your key never leaves your machine" promise the product already makes to users in onboarding copy. The recon's job is to make this choice concrete; the decision is the founder's.
1. Evidence — what PR5 actually shipped for BYO-key (D1)
ModelGate.tsx is the single shared "get a working model" component — mounted in onboarding step 3 AND Settings → Models (apps/web/src/components/os/model-gate/ModelGate.tsx:8–20 header doc).
- BYO-key cloud path: pick provider → paste key → live-validate (
adapter.testApiKey(..., { live: true }), line 94) → write to Vault (adapter.setProviderKey, line 99). - Explicit BYO framing in the UI: "Bring your own key — it's stored encrypted in your Vault and never leaves your machine." (line 186).
- Honesty contract already enforced: "verified" only after a live probe; a format-only pass says "looks valid (not live-verified)" (lines 18–20, 247–253).
- Local path (Ollama) is the other route to a working model — also zero cost to Waggle (lines 266–304).
Onboarding hard gate (D2): ModelGateStep.tsx disables "Continue" until useHasWorkingModel is true (apps/web/src/components/os/overlays/onboarding/ModelGateStep.tsx:49), with copy "Bring your own provider key … Nothing leaves your machine without your key." (lines 26–28) and one soft escape ("I'll do this later" → Home NoModelBanner, lines 9–17).
Interpretation: the user pays the provider directly. Waggle's margins are clean; it never carries inference cost. This is textbook Option A (BYO-key) from DESIGN_POV §4 (docs/design_handoff_waggle_app/DESIGN_POV.md:62–70).
2. Evidence — Stripe tiers are flat SUBSCRIPTION, not metered
packages/shared/src/tiers.ts — canonical 5-tier system (TRIAL/FREE/PRO/TEAMS/ENTERPRISE), pricing in the header doc (lines 7–18): flat per-seat/per-month dollar amounts. stripePriceId is a single price per tier (lines 122, 143) — a fixed recurring price, not a metered/usage price.
Desktop sidecar Stripe (packages/server/src/stripe/):
checkout.ts:40—mode: 'subscription',line_items: [{ price: priceId, quantity: 1 }]. Quantity 1, fixed price. (Only PRO/TEAMS, line 25.)webhook.ts:114–158— handles exactly 3 subscription lifecycle events:checkout.session.completed,customer.subscription.updated,customer.subscription.deleted. Noinvoice.created/ usage-record handling. On cancel → tier drops to FREE (line 150).index.ts:71–100—tierFromPriceId/priceIdForTiermap fixed monthly/annual price IDs to tiers. Pure subscription mapping.portal.ts:41—stripe.billingPortal.sessions.create— defers payment-method / invoice / cancel management to Stripe's hosted Customer Portal (this is where invoices and payment methods legitimately come from, never invented locally).
apps/www cloud port (already built — see §4):
app/api/stripe/checkout/route.ts:170—mode: 'subscription', fixedpriceIdresolved by env orlookup_key${tier}_${billing}(lines 109–119).app/api/webhooks/stripe/route.ts:196–205— same 3 subscription events, mirrored to ClerkpublicMetadata. No metering.
Conclusion: there is no metered/usage-based Stripe billing anywhere. The model is "pay a flat monthly fee for capabilities (workspaces, connectors, governance), not for inference." Inference is on the user's own key/quota.
3. Evidence — the current Usage/cost surface (what's shown today)
There is NO UsageApp.tsx. The de-facto Usage screen is TelemetryApp.tsx — titled "Usage & cost" in the UI (apps/web/src/components/os/apps/TelemetryApp.tsx:159). PR6b's "Usage/budget" screenshot (docs/redesign-warm-hive/smoke-pr6b-20260618/04-usage-budget.png) is this surface.
What it shows (all read-only, estimate-based — never a balance to draw down):
- Total tokens, estimated cost, by-model spend, by-workspace (TEAMS-gated) — from
GET /api/cost/summaryand/api/cost/by-workspace(TelemetryApp.tsx:55–100). - A daily budget the user can set, which produces a soft warning at 80% / "exceeded" status (
TelemetryApp.tsx:151,192–195) — purely advisory.
The backing route confirms the "estimate, not meter, not enforce" nature:
packages/server/src/local/routes/cost.ts:8–9— "Data source: in-memory CostTracker … All cost values are estimates based on published model pricing."- Returns
estimatedCosteverywhere (lines 175, 181, 188). - The daily-budget "exceeded" status (lines 162–169) sets a string status only — nothing in the codebase blocks a request when exceeded. It's a dashboard, not a quota gate.
- Free for all tiers per a product decision (line 263, "P22 … usage/telemetry info is free for all tiers").
Interpretation: today's Usage tells the user "here's roughly what your own provider key is costing you" — a BYO-key courtesy readout. It is structurally NOT a metered-balance/credits surface.
4. Evidence — Auth (Clerk) status: NOT in the desktop app; FULLY built in apps/www
Desktop apps/web has no Clerk and no real logged-in identity. Grep for Clerk|@clerk|SignIn|auth0 across apps/web/src → no files. "Identity" today = a tier field in config.json, read fail-closed-to-FREE by readTierFromDataDir (packages/server/src/middleware/assert-tier.ts:21–32). There is a data-model placeholder: User.clerkId: string exists in packages/shared/src/types.ts:6, but nothing populates it from a real Clerk session in the desktop path.
apps/www (Next.js cloud/landing) already has a complete, themed Clerk + Stripe SaaS surface — this is the direct reference (and possibly the literal home) for PR7's screens 13/14:
apps/www/app/sign-in/[[...sign-in]]/page.tsx—<SignIn />Clerk component, themed via<ClerkProvider>(header doc line 15).apps/www/app/sign-up/[[...sign-up]]/page.tsx— sign-up (verified to exist via glob).apps/www/middleware.ts:6,16—clerkMiddleware()wired, matcher includes API routes.apps/www/app/account/page.tsx— account surface.apps/www/app/api/stripe/checkout/route.ts— lazy-create Stripe Customer → store id in ClerkpublicMetadata(lines 81–100); subscription checkout (line 170).apps/www/app/api/webhooks/stripe/route.ts— mirrors subscription state Stripe → Clerk metadata (3 events).apps/www/app/_components/Pricing.tsx— pricing cards.
Net for PR7 Auth (screen 13): in apps/www, Auth is REAL and only needs theming to the warm-Hive tokens. In the desktop apps/web, Auth is MUST-BUILD if the desktop must show a real logged-in identity (otherwise the design's own line "An account is optional — Waggle runs fully local without one" — SCREENS.md:274 — means desktop can stay identity-light and route account/billing to the cloud apps/www). This is itself a sub-decision the founder should confirm: does screen 13 live in apps/www only, or also in the desktop shell?
5. The screen-14 fabrication risks (honesty contract carried from PR3–PR6)
SCREENS.md:282–295 (screen 14 Billing) calls for four states. Three of them name fields that must come from Stripe, never be invented:
| Field in the design | Risk | Required gating |
|---|---|---|
Invoices (Paid + PDF) (SCREENS.md:292) |
Fabricating an invoice list / fake PDFs | Source ONLY from Stripe Customer Portal (portal.ts already does this) — do NOT render a local invoice list. If portal isn't reachable, show "Manage in Stripe" link, not a stub table. |
Payment method "VISA ···4242" (SCREENS.md:291) |
Hardcoding a fake card (the 4242 test card is literally in the spec text) |
Never render a card brand/last4 the app doesn't have from Stripe. The portal owns this. The 4242… in the design is a mockup placeholder — it must not ship as real-looking data. |
"Next charge" / billing cycle (SCREENS.md:291–292) |
Inventing a renewal date | Only from Stripe subscription data via the portal. |
Checkout card form (email/card/expiry/CVC) (SCREENS.md:287–289) |
Building a fake in-app card form that collects nothing real | Use Stripe Checkout (hosted) — the design itself says "Use Stripe Checkout/Customer Portal where possible" (SCREENS.md:294). The in-app form mock is illustrative; real PCI capture is Stripe's. |
| Logged-in identity / avatar+name (screen 13) | Showing a name/email for a session that isn't real | Bind to the real Clerk session (apps/www) or render the honest "no account / local-first" state (SCREENS.md:274). The desktop's userName={null} → "Account"/"W" pattern is the honest fallback (BUILD-PLAN §9 deferred note, line 191). |
Plus a Usage-screen trap if Option B is ever pursued: a credits/balance number, a "you've used X of Y tokens" quota bar, or a "$N remaining" figure would all be fabricated today (no ledger exists). The current estimate-only dashboard (§3) is the honest ceiling — do not dress it up as a metered balance.
6. The decision, made concrete
Option A — Ratify BYO-key + flat subscription (RECOMMENDED, de-facto current state)
PR7 Billing themes the existing Stripe subscription flow; near-zero new backend. Exactly what's needed:
- Auth (screen 13):
- Cloud (
apps/www): theme the existing<SignIn/>/<SignUp/>Clerk components + the brand split-panel to warm-Hive tokens. Add the local-first trust copy ("an account is optional"). ~UI-only. - Desktop (
apps/web): confirm whether it needs a real auth surface at all (§4 sub-decision). If "local-first, no account" stands, desktop screen 13 is a deep-link to the cloud account page + the honest no-account state — minimal build. If a real desktop session is wanted, that's the one genuine new piece (embed Clerk in the SPA / token bridge) — flag as a scoped add-on, not core to Option A.
- Cloud (
- Billing (screen 14):
- Plans state: theme to tokens; data already exists (tiers.ts,
useBilling.startCheckout). Add monthly/annual toggle UI (the −20% annual already exists as price IDs —index.ts:91–100). - Checkout state: redirect to Stripe Checkout (already wired both surfaces). The "in-app card form" from the design ships as a themed intro/summary, then hands off to Stripe — no PCI surface built.
- Success state:
useBillingalready syncs?session_id=post-checkout (useBilling.ts:103–115). Theme the success ring/receipt; receipt link → Stripe. - Manage state:
billing.openPortal()already exists (useBilling.ts:84,SettingsApp.tsx:625) → Stripe Customer Portal owns invoices/payment-method/cancel. Theme the entry; do NOT build a local invoice/card UI (§5). - Usage: leave the estimate-only "Usage & cost" dashboard as-is; optionally reskin to warm-Hive in the long tail. No metering.
- Plans state: theme to tokens; data already exists (tiers.ts,
- Net new backend for Option A: essentially none. Possibly: thread
STRIPE_PRICE_*_ANNUALinto the desktop checkout UI's monthly/annual toggle (the resolver already supports it —index.ts:91), and (if desktop auth is wanted) a Clerk-session bridge. Otherwise pure theming + wiring existing routes to the new screens.
Option B — Pivot to Waggle-metered (MAJOR ARC, strategic reversal)
Enumerated NEW plumbing (none of this exists today):
- Inference-cost metering per request — a real, persisted, authoritative usage ledger (today's CostTracker is in-memory + estimate-only,
cost.ts:8; it would need to become durable, exact, and per-user/account). - Waggle holds the provider keys — a managed model pool where Waggle's own key pays the provider. This directly contradicts shipped onboarding/Settings copy ("your key never leaves your machine") and the local-first promise — a product-positioning reversal, not just code. (
managedModelPoolcapability exists as a flag in tiers.ts:97/130 but has no inference-path implementation behind it.) - Usage caps / quota enforcement — convert the advisory budget (
cost.ts:162–169, soft warning only) into a hard gate that blocks chat requests at the inference path when a balance/quota is exhausted. New enforcement point in the agent loop. - Stripe metered/usage-based billing —
mode: 'payment'top-ups or metered subscription items +createUsageRecord/billing-meter reporting. New webhook events (invoice.created, usage aggregation). None of the current 3-event handlers (webhook.ts) cover this. - A credits/balance surface — a new "$N remaining / buy more credits" screen + the ledger behind it. (None exists; building it without the ledger would be fabrication — §5.)
- Usage screen rework — from "here's your own-key estimate" to "here's your metered balance, draw-down, and top-up" — a full rebuild of
TelemetryApp. - Margin/abuse controls — rate limits, anti-abuse, cost-of-goods accounting that the BYO model never needed because Waggle carried no inference cost.
This is a multi-month arc that reshapes Billing, Onboarding (the model gate would invert — from "add your key" to "you're metered"), Usage, and the core inference path, and it takes on inference COGS + abuse risk that the current architecture deliberately avoids.
Recommendation
Ratify Option A. Rationale: (1) the codebase has already committed to it end-to-end (BYO-key gate + flat subscription + no metering), so A is "finish what's shipped," (2) it keeps the local-first / "your key never leaves your machine" promise the product already makes to users in onboarding, (3) it keeps margins clean (no inference COGS), and (4) PR7 collapses to theming + wiring existing routes. Option B is a deliberate strategic pivot with real COGS, abuse surface, and a contradiction of live product copy — worth a separate, founder-led decision, not something PR7 should absorb. DESIGN_POV §4 said "the current design supports either but commits to neither" (DESIGN_POV.md:70); the implementation has since committed to A. PR7 should make that commitment explicit and themed.
7. Open sub-decisions for the founder (surfaced, not decided)
- Does screen 13 (Auth) live in
apps/wwwonly, or also in the desktopapps/web? Desktop has no Clerk today; the design says accounts are optional. If desktop stays identity-light, PR7 desktop-Auth is a deep-link + honest no-account state (cheap). If a real desktop session is wanted, add a scoped Clerk-bridge task. - Monthly/annual toggle on the desktop Billing tab — the annual price resolver already exists (
index.ts:91); the desktop UI currently only callsstartCheckout('PRO'|'TEAMS')with default monthly (SettingsApp.tsx:584). Adding the toggle is small but is genuinely new desktop UI. - Where does screen 14 Billing render? The richest, already-real flow is in
apps/www(Clerk-linked). The desktop SettingsApp Billing tab is a thinner subscription surface. PR7 could (a) theme both, or (b) make desktop Billing a deep-link to the cloud account page. Confirm.
Appendix — files read for this recon (all file:line claims above traceable to these)
docs/design_handoff_waggle_app/DESIGN_POV.md(§4, lines 62–70)docs/redesign-warm-hive/BUILD-PLAN.md(§6 PR7 row line 145; §7 #5 line 165; §9 deferred note line 191)docs/design_handoff_waggle_app/SCREENS.md(screen 13 lines 269–278; screen 14 lines 282–295)packages/shared/src/tiers.ts(5 tiers, single stripePriceId per tier)packages/shared/src/types.ts:6(User.clerkIdplaceholder)packages/server/src/stripe/{index,checkout,webhook,portal}.ts(subscription-only)packages/server/src/middleware/assert-tier.ts(config.json tier, no real identity)packages/server/src/local/routes/cost.ts(estimate-only, advisory budget)apps/web/src/components/os/model-gate/ModelGate.tsx(BYO-key, shipped)apps/web/src/components/os/overlays/onboarding/ModelGateStep.tsx(hard gate)apps/web/src/components/os/apps/TelemetryApp.tsx("Usage & cost" surface)apps/web/src/hooks/useBilling.ts(checkout/portal/sync, subscription)apps/web/src/components/os/apps/SettingsApp.tsx(Billing tab, upgrade/portal)apps/www/{middleware.ts, app/sign-in/.../page.tsx, app/api/stripe/checkout/route.ts, app/api/webhooks/stripe/route.ts}(Clerk + Stripe subscription, already built — PR7 reference)