Credits & billing

Who pays for the AI, how it is metered, and what happens when a run fails · ← platform hub

In one sentence

Generating a course costs real money at an LLM provider, so every AI run is paid for in credits: users-auth holds the balance and the Stripe subscription, authoring-v2 debits before starting work and refunds if it fails, and each worker reports what it actually consumed so the estimate can be reconciled against reality.

Subscription

users-auth owns the commercial relationship: Stripe subscriptions, the webhook that keeps them in sync, plan entitlements and the tenant's credit balance. An entitlements reconciler runs in-process to keep the platform's view aligned with Stripe rather than trusting a single webhook delivery. Enterprise contracts get a welcome email via auth.admin.enterprise_welcome (users-auth-service/app/routes/admin_billing.py:199).

Debit, refund, settle

sequenceDiagram
  autonumber
  participant A as Author
  participant AV2 as authoring-v2
  participant UA as users-auth
  participant W as pipeline worker
  A->>AV2: start a run
  AV2->>UA: debit credits
  UA-->>AV2: ok (or refuse)
  AV2-->>W: work requested
  W-->>AV2: authoring.usage.event
  alt run fails
    AV2->>UA: refund
  else run succeeds
    AV2->>AV2: settle against actual usage
  end
  1. Estimate and debit. Before any worker is asked to do anything, authoring-v2 asks users-auth to debit the estimated cost (authoring-service-v2/app/core/http.py:463-548). A tenant without balance is refused here, so no LLM spend happens.
  2. Do the work. Every pipeline worker reports its real consumption as authoring.usage.event on the AUTHORING_USAGE stream — five of the six workers publish it, and authoring-v2's usage consumer is the only reader (authoring-service-v2/app/consumers/register.py:68).
  3. Refund or settle. A failed run is refunded. A successful one is settled against actual usage by a background sweep, and a reconciliation loop looks for drift between what was charged and what was consumed.
  4. Publishing costs too. micro-learning debits credits at publish, and narration has its own credit estimate surfaced to the author before they commit to generating audio.

Metering the workers

One service holds the OpenRouter account key: suggest-pipeline-worker. Other services that need a metered LLM call go through its internal endpoints rather than holding the key themselves. That is a deliberate boundary — it keeps the credential in one place and gives a single choke point for metering — and it is why suggest-pipeline sits on a user-blocking path despite being a background worker.

A credits monitor loop inside that worker watches the provider balance and is meant to raise an alarm before the account runs dry.

Where it breaks today

IssueDetail
The low-credit alarm is not wiredauthoring.usage.credits_low is published to AUTHORING_USAGE, but the only consumer filters authoring.usage.event exactly, not authoring.usage.>. The message is retained for 30 days and never read. A log line is the only working low-balance signal. See the suggest-pipeline verdicts.
Reconciliation drift is reported to nobodyauthoring.usage.reconciliation_drift has no consumer — the loop detects a discrepancy between charged and consumed credits and publishes it into the void. See the authoring-v2 verdicts.
Nothing scrapes the metrics endpointauthoring-v2 exposes /metrics with cost counters, but no ServiceMonitor, PrometheusRule or scrape annotation exists in the repo. The alert rules in its cost-monitoring doc are a snippet, not a deployed object.
A grant endpoint nobody callsPOST /v1/internal/credits/grant on users-auth has no caller — both credit clients only build debit and refund. Granting credits is presumably manual today. See the users-auth verdicts.