Skip to content

feat(cloud): managed AI metering, billing & cost truth, run-in-public metrics (0200) - #181

Merged
crs48 merged 15 commits into
mainfrom
feat/0200-cloud-billing-ai-metering
Jun 18, 2026
Merged

feat(cloud): managed AI metering, billing & cost truth, run-in-public metrics (0200)#181
crs48 merged 15 commits into
mainfrom
feat/0200-cloud-billing-ai-metering

Conversation

@crs48

@crs48 crs48 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Implements exploration 0200 — getting xNet Cloud billing into a really good place (with a managed, metered AI offering) and a public run-the-company-in-public metrics dashboard. The key insight from the exploration: the AI/billing/cost engine in @xnetjs/cloud was already built but dormant (zero consumers) — this mostly wires + productizes it.

Slice A — Managed AI (the money-making piece)

  • @xnetjs/entitlements: includedAiUsd + aiMonthlyBudgetUsd per plan, carried in the signed HUB_PLAN token (withAiBudget).
  • Durable UsageLedger over the PR feat(cloud): integrate real M1+M2 credentials (Stripe, Firestore, Cloud Run) #175 DocStore, with an optional sinceMs so the budget resets monthly.
  • LiteLLMKeyManager (port + fake + real adapter) mints each aiEnabled tenant a budgeted virtual key at provision time; updated on plan flip, revoked on delete.
  • POST /ai/chat over the dormant MeteredGateway: budget hard-stop → meter → Stripe. Over-budget tenants get a 402 with no provider call. Env-wired via AI_GATEWAY_BASE_URL (LiteLLM or OpenRouter upstream); a 1.25× markup pricing table.

Slice B — Billing & cost truth

  • Self-serve POST /account/plan (in-tier flip live; tier cross → migration notice).
  • Dashboard: a Managed-AI usage meter (used / included / cap) + a plan-change card; a "Billing & AI usage" row in the web app settings.
  • @xnetjs/cloud/cost margin reconciliation (measuredCogs/reconcileTenantMargin/aggregateMargin) turns the assumed COGS model into measured per-tenant margin and flags money-losing tenants.

Slice C — Run-in-public dashboard

  • buildCompanyMetrics rollup with a k-anonymity floor + break-even; a /open Astro page with dependency-light inline-SVG charts (customer growth, MRR, weekly cost stack), fed by a committed metrics.json (git history = transparency log) + a hand-maintained opex.ts.
  • scripts/cloud-metrics-rollup.mjs publishes with a defense-in-depth k-anon re-check + a ban on per-customer fields.

Verification

  • 189 unit/integration tests green (entitlements, cloud, apps/cloud); full-graph turbo build typechecks clean; eslint/prettier clean; lockfile untouched (no new deps).
  • /open preview-verified (charts render, no console errors). Sample figures are clearly flagged until the live P&L is wired.

Operator/follow-up (documented in docs/cloud/SETUP.md Part 3): deploy the LiteLLM proxy, the hub→control-plane AI forwarder + client managed provider, the live usage collector, and the metrics cron. Doc checklist updated.

🤖 Generated with Claude Code

xNet Test and others added 12 commits June 17, 2026 18:17
…ic dashboard

Two-part exploration (0200): (1) get xNet Cloud billing into a really good place —
managed AI with metered usage billing, accurate per-tenant cost calculation, and
self-serve account management; (2) a public "run the company in public" dashboard
on the marketing site (customer/revenue growth WoW + honest cost breakdown +
break-even). Key finding: the @xnetjs/cloud AI/billing/cost spine (LiteLLM gateway,
MeteredGateway, usage ledger, token pricing, Stripe meters, COGS model) is already
built and tested but DORMANT — zero consumers — so this is mostly wiring +
productization. Recommends three slices (Managed AI → cost truth → public dashboard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Usd)

Managed AI needs an included monthly spend and a hard cap per plan, carried in
the signed HUB_PLAN token so hub/control-plane enforce it without a round-trip
(exploration 0200, slice A). aiEnabled stays as the on/off flag; the new numbers
drive the dashboard "used / included / cap" display, the metered gateway's hard
stop, and the Stripe metered Price's free first tier. Adds withAiBudget() flip
(cap must be >= included) + tests; the token round-trip already carries new
fields since it serializes the whole entitlements object.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AI usage ledger is authoritative for accrued spend, so it must survive a
control-plane restart — back it with the same DocStore port as tenants/bindings
(usageLedgerFromDocs, keyed by the idempotency key). Extend UsageLedger with an
optional sinceMs so totalChargeUsd/entries can scope to a billing period: this is
how a *monthly* AI budget resets each period instead of accruing for life.
MemoryUsageLedger honors it too; Stripe's identifier dedup remains the backstop
against a read-then-write race. (Exploration 0200, slice A.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each aiEnabled tenant gets its own LiteLLM virtual key carrying a hard budget the
proxy enforces per window. VirtualKeyManager port with FakeVirtualKeyManager
(deterministic keys, no proxy) and LiteLLMKeyManager (thin wrapper over
/key/generate|update|delete with an injected fetch + master key). The control
plane creates one at provision time and stores it server-side as the gateway's
Bearer credential — it never reaches the client. (Exploration 0200, slice A.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the dormant MeteredGateway into a live control-plane route (exploration
0200, slice A). A tenant's hub forwards a chat request; the route runs it through
the budget hard-stop → meter → Stripe pipeline and returns the answer plus
spend-this-period for the "used / included / cap" display. Over-budget tenants get
a 402 with no provider call. resolveTenant is injected so hub→control-plane auth
is pluggable and the route is testable with fakes. Also extends MeteredGateway
with an optional periodStartMsFor so the budget scopes to the billing month.
Mounted in createControlPlaneApp only when AI deps are configured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the managed-AI provisioning seam (exploration 0200, slice A):
- TenantRecord gains aiKeyRef (LiteLLM virtual key, server-side) + stripeCustomerId.
- ControlPlane mints a budgeted virtual key for an aiEnabled tenant at provision
  time, updates the budget on an in-tier plan flip, and revokes on delete; budget
  + included come from the entitlement token. currentPeriodStartMs gives the
  monthly budget window.
- Env wiring: aiChatDepsFromEnv assembles the metered route (GatewayClient +
  durable usageLedgerFromEnv + Stripe meters + a markup pricing table +
  internal-secret tenant resolver) only when LITELLM_BASE_URL is set;
  aiKeysFromEnv selects the LiteLLM key manager; both null-fallback like PR #175.
- Refactor firestoreFromEnv out so the usage ledger shares one Firestore client.
109 apps/cloud tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lf-serve plan change

Slice B (exploration 0200) — billing & cost truth, server side:
- @xnetjs/cloud/cost reconcile: measuredCogs/reconcileTenantMargin/aggregateMargin
  turn estimateCogs's *assumed* inputs into *measured* per-tenant margin from what a
  tenant actually used (storage bytes, active hours, AI provider cost, real Stripe
  fees) vs the revenue it produced; flags negative-margin tenants. Pure + tested.
- Dashboard: a Managed-AI usage meter (used / included / cap, near-cap warning) fed
  by the shared usage ledger, plus a self-serve "Change plan" card.
- POST /account/plan (session-authed): an in-tier change flips live; a tier crossing
  returns a migration notice instead of silently moving data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Billing & AI usage" row to the existing xNet Cloud group in Network
settings, deep-linking to the control-plane dashboard's billing + managed-AI
usage view (kept server-side so no payment UI/secrets enter the client bundle).
Reinforces the two-identity model in the copy. (Exploration 0200, slice B.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice C (exploration 0200):
- apps/cloud metrics rollup: buildCompanyMetrics joins weekly tenant/MRR/COGS with
  opex, SUPPRESSES any week below a k-anonymity cohort floor, and computes
  cumulative break-even. Pure + tested. Exported for the control plane to emit.
- site /open page: "Running the company in the open" — inline-SVG charts (no
  charting lib) for customer growth, MRR growth, weekly cost stack, and an honest
  break-even framing, fed by a committed metrics.json (git history = transparency
  log) + a hand-maintained opex.ts. Footer link added.
- scripts/cloud-metrics-rollup.mjs: publishes the snapshot with a defense-in-depth
  k-anon re-check + a ban on any per-customer field, then prompts for a review PR.
Verified: site builds, /open renders all charts with no console errors. Sample
figures are clearly flagged until the live P&L is wired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reconcile the chat-path env var to the canonical AI_GATEWAY_BASE_URL (LiteLLM or
  OpenRouter); LITELLM_BASE_URL/MASTER_KEY drive the LiteLLM-specific key admin.
- Add LITELLM_MASTER_KEY / AI_MARKUP / AI_ALLOWED_MODELS to the env schema.
- SETUP.md Part 3: deploy the LiteLLM proxy, wire managed-AI pricing/budget, the
  Stripe metered Price, and the run-in-public metrics publish step.
- Check off the implemented 0200 checklist items; mark the LiteLLM deploy, hub
  proxy + client managed-provider, live usage collector, and the metrics cron as
  the remaining operator/follow-up steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-181 June 18, 2026 02:08 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

@crs48
crs48 temporarily deployed to pr-181 June 18, 2026 02:09 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

Screens

✏️ Settings _(SSIM 0.932)_
before after diff
before after diff

Auto-captured by CI · run. Informational — not a blocking check.

github-actions Bot added a commit that referenced this pull request Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #181.

github-actions Bot added a commit that referenced this pull request Jun 18, 2026
Comment thread site/src/data/metrics.ts
{ label: 'Overhead', usd: latest.costs.otherUsd, color: '#9ca3af' }
]

export const updated = metrics.updated
Comment thread site/src/data/opex.ts
note?: string
}

export const updated = 'June 2026'
nowMs: () => number
): (c: Context) => Promise<AiTenantContext | null> {
const secret = env.XNET_CLOUD_INTERNAL_SECRET
return async (c) => {
Comment thread site/src/data/metrics.ts
costs: { infraUsd: number; payrollUsd: number; saasUsd: number; otherUsd: number }
}

export interface CompanyMetrics {
Comment thread site/src/data/metrics.ts
w.costs.infraUsd + w.costs.payrollUsd + w.costs.saasUsd + w.costs.otherUsd

/** Weekly revenue ≈ MRR / 4.345 (matches the rollup's break-even math). */
export const weekRevenue = (w: CompanyMetricsWeek): number => w.mrrUsd / 4.345
Comment thread site/src/data/metrics.ts
export const weekRevenue = (w: CompanyMetricsWeek): number => w.mrrUsd / 4.345

/** Percent change between the two most recent weeks for a numeric selector. */
export function wow(select: (w: CompanyMetricsWeek) => number): number {
Comment thread site/src/data/opex.ts
note?: string
}

export const updated = 'June 2026'
Comment thread site/src/data/opex.ts
export const updated = 'June 2026'

/** Recurring monthly operating costs. Infra here is the fixed floor; usage-based infra rides the weekly COGS. */
export const OPEX: OpexLine[] = [
Comment thread site/src/data/opex.ts
export const monthlyOpexTotal = OPEX.reduce((sum, l) => sum + l.monthlyUsd, 0)

/** Recurring opex grouped by category (for the breakdown chart). */
export const opexByCategory: { category: OpexCategory; monthlyUsd: number }[] = (
The footer was missing the cloud surfaces entirely. Add a dedicated "Cloud"
column (xNet Cloud /cloud · Pricing /cloud/pricing · Open metrics /open) and put
Changelog under Product, so every public section the nav exposes also has a home
in the footer. Grid widened to 6 columns on desktop, 2 on small screens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-181 June 18, 2026 02:27 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jun 18, 2026
github-actions Bot added a commit that referenced this pull request Jun 18, 2026
@crs48
crs48 merged commit c4db678 into main Jun 18, 2026
11 of 12 checks passed
@crs48
crs48 deleted the feat/0200-cloud-billing-ai-metering branch June 18, 2026 02:35
github-actions Bot added a commit that referenced this pull request Jun 18, 2026
crs48 added a commit that referenced this pull request Jun 18, 2026
## Summary

Turns the OpenRouter key into working **metered managed AI** for xNet
Cloud, and adds the billing-model pieces from exploration **0201**.
Builds directly on the 0200/#181 metering spine — the gateway and key
adapters slot into the existing `ChatGateway` / `VirtualKeyManager`
ports, so the route, ledger, Stripe meter, dashboard, and margin
reconciliation are untouched.

### Phase 1 — OpenRouter gateway + exact cost
- **`OpenRouterGatewayClient`** (`ChatGateway`): OpenAI-compatible
`/chat/completions`, requests usage accounting, and reads
**`usage.cost`** → `ChatResult.providerCostUsd` (the exact USD billed,
caching/reasoning tokens baked in).
- **`OpenRouterKeyManager`** (`VirtualKeyManager`): per-tenant keys via
the Provisioning API with a monthly USD `limit`. The secret is the
Bearer; update/delete address the key by `hash`, carried as
`VirtualKey.manageId` and stored on `TenantRecord.aiKeyManageRef`.
- **Exact-cost metering:** `meterUsage` charges off `providerCostUsd ×
markup` (rounded up) when present, else the static-table estimate — so
margin reconciliation becomes *measured*, not modeled. New
`computeChargeFromCostUsd`.
- **Env selection:** `aiGatewayProvider()` picks `openrouter | litellm`
from `AI_GATEWAY_PROVIDER` or a base-URL sniff; `aiKeysFromEnv` builds
the OpenRouter key manager from `OPENROUTER_MANAGEMENT_KEY`.
- Default `AI_MARKUP` 1.25 → **1.3** (absorbs OpenRouter's ~5.5% buy-in
+ Stripe fees). SETUP.md §3a rewritten with OpenRouter as the
recommended path.

### Phase 2 — billing model (server pieces)
- **Self-serve spend cap:** `ControlPlane.setAiCap` (clamped to ≤ the
plan's `aiMonthlyBudgetUsd`); the tenant resolver enforces `min(cap,
plan cap)`.
- **Threshold logic:** `aiBudgetStatus`
(`included|overage|near-cap|over-cap`) + `crossedThresholds`
(50/80/95/100%); `/ai/chat` now returns `budgetState` for the client
gauge.

### Deferred (honest follow-ups, consistent with how #181 deferred its
Phase 3)
- Prepaid **credit packs** (Stripe Credit Grants + burn-down + checkout
UI)
- Notification **delivery** for threshold crossings (calculation lands
here; no transport yet)
- Per-seat included AI for Team+
- **Phase 3** wire-into-product: hub `/ai/chat` forwarder + `managed`
client `AIProvider` + budget gauge UI
- Operator: deploy + live smoke; mirror the gitignored `.env.staging`
key to Secret Manager

## Decision notes
- **OpenRouter-direct over self-hosted LiteLLM** for our scale: no proxy
to run, exact cost, managed budgets. LiteLLM stays a documented swap-in
behind the same ports. Stacking LiteLLM in front of OpenRouter is
explicitly avoided (double fee + diverging cost numbers).
- The user's "pass-through per token" vs "prepaid credits with a max"
are the same pattern at two points on one line — included allotment →
overage. Default = PAYG-metered-to-a-hard-cap (already built); credits
are the deferred add-on.

## Testing
- `255` unit tests pass across `packages/cloud` + `apps/cloud` (new:
OpenRouter gateway/keys, exact-cost metering, budget status/thresholds,
provider/key-manager selection, `setAiCap`).
- `@xnetjs/cloud` and `xnet-cloud` typecheck clean; prettier + eslint
clean on touched files.

Exploration:
`docs/explorations/0201_[_]_OPENROUTER_LITELLM_METERED_AI_AND_CREDITS_BILLING.md`
(phase 1–2 items checked off).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48 added a commit that referenced this pull request Jun 20, 2026
…08) (#223)

Implements exploration
[0208](docs/explorations/0208_[x]_OPENROUTER_MANAGED_AI_MODEL_SWITCHING_AND_CLIENT_WIRING.md):
make OpenRouter **managed AI** a first-class client experience — route
the app's existing AI surfaces through XNet Cloud's metered gateway, and
add real **model switching**.

The metered-billing half already shipped (0200 #181, 0201 #187: gateway,
exact `usage.cost`, per-tenant keys, budget cap). This PR closes the two
pieces those docs deferred: **client wiring** and **model switching**.

## Phase 1 — managed client path (`1c928717`)
- **`ManagedProvider`** (`@xnetjs/plugins`): keyless `AIProvider` that
posts to the hub's `/ai/chat`, surfaces the live budget (`onBudget`),
and maps `402` → typed `AiBudgetError`. Wired into `createAIProvider` as
a new `'managed'` `AIProviderType`.
- **`'managed'` connector tier** + detection (`probeManaged` →
`/ai/health`), preferred when available, hides off-cloud (BYO stays the
OSS path). Connector mapping + USABLE tiers updated.
- **`aiForwarderFeature`** (`@xnetjs/hub`): authed proxy that injects
the per-tenant credential (`x-internal-secret` + `x-tenant-id`) to the
control plane — the client never holds a key. Generic over injected
fetch, no hub→cloud edge; mirrors `connectorSyncFeature`.
- **Chat panel**: managed controls + "used / included / cap" budget
gauge.

## Phase 2 — model switching (`c6ddd4de`)
- **Per-plan model gating**: `PlanEntitlements.aiModels` (`'all' |
id[]`) + `aiDefaultModel`; cheap plans get a cheap subset, bigger plans
the whole catalog. `withAiModels` / `aiModelAllowed` helpers.
- **Live model catalog**: `apps/cloud/src/ai/models.ts` proxies
OpenRouter `/models` → priced `ModelCard[]` with a TTL cache
(single-flight + stale-while-revalidate).
- **`GET /ai/models`**: cached catalog ∩ plan policy + default; id-only
fallback when the catalog is unavailable. `/ai/chat` now defaults to the
plan model and enforces the plan policy.
- **Model picker UI**: data-driven dropdown grouped by family with
$in/$out + context badges; preselects the plan default.

## Phase 3 — reliability (`c511eeca`)
- **Model fallbacks**: `ChatRequest.fallbackModels` → OpenRouter
`models:[primary,…]`; `/ai/chat` forwards only plan-permitted fallbacks;
served model reported back.
- Dropped the deprecated `usage:{include:true}` flag (OpenRouter always
returns `usage.cost` now).

## Tests
132 new/changed tests across the touched files (managed provider, hub
forwarder, connector detection/mapping, model catalog + cache, route
gating/default/fallback, entitlements gating, model-picker helpers). All
green locally.

## Notes
- Self-host degrades gracefully: no control plane ⇒ `managed` tier
hides, BYO-key works.
- Streaming `/ai/chat` and deeper editor/agent wiring are explicitly
deferred in the doc.
- Managed-AI hub env documented in `docs/cloud/SETUP.md`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants