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
- 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.
- Do the work. Every pipeline worker reports its real consumption as
authoring.usage.eventon theAUTHORING_USAGEstream — 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). - 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.
- 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
| Issue | Detail |
|---|---|
| The low-credit alarm is not wired | authoring.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 nobody | authoring.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 endpoint | authoring-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 calls | POST /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. |