Skip to main content

Payment / Billing Logic and Usage Cost Guardrails (Translated from Chinese)

Based on the latest code and docs as of 2026-06-24 (cross-referenced against api/src/billing/, api/src/conversations/, and the ops frontend). English authoritative version: billing-and-cost-guardrails.md (if the two diverge, defer to the English version + code). Terminology follows the repository GLOSSARY.md: Specialist = AI persona, Expert = HP human reviewer, AM = Account Manager, client = org.

TL;DR

  • Billing = a fixed monthly fee per "assigned Specialist instance", entirely usage-independent.
  • Only two things ever hard-block activity: per-org message rate limiting (anti-abuse, unrelated to cost) and suspension for non-payment (dunning). Neither is tied to cost.
  • Optional human Expert correction/follow-up work is currently neither metered, priced, nor capped. LLM token cost is now visible to ops but not enforced. There is no per-client cost cap, and no gross-margin guardrail.

1. Billing Model (how money is collected)

Billing unit: one fixed monthly fee per assigned Specialist instance (per org_specialist_assignment / OSA).

  • The rate for each (Org × Specialist) assignment lives in org_specialist_assignment.monthly_rate_config.base_rate, corresponding to one Lago subscription.
    • Provider = Lago (self-hosted); Stripe is only the downstream payment processor (PSP), not the billing ledger.
    • The subscription is created when the AM assigns a Specialist (SyncSpecialistSubscriptionCommand).
  • One invoice per org, itemized by Specialist line; prorated daily; mid-cycle rate changes go through a Lago upgrade/downgrade and are written to org_specialist_assignment_rate_history.
  • Reuse amplifies revenue rather than diluting it: the same catalog prototype assigned to N clients = N independent operating instances = N monthly fees (each with its own OSA + subscription).
  • Not billed: LLM token cost, message volume, conversation count, Expert hours. The monthly fee is usage-independent — no tier, no included quota, no overage. The old Stripe tiered-plan model has been removed.

2. Payment & Access Gate (dunning)

  • Payment methods: card or invoice; the billing org role can update the billing email (@OrgRoles("admin","owner","billing")).
  • The only hard access gate = non-payment: payment failure → grace period (default N days) → BillingAccessGateService.runGate() sets the org status to suspended. This is a suspension based on payment status, not usage or cost.
  • Members with owner / admin / billing roles are notified on payment failure (#1475).

3. Cost Visibility (observe only, not enforced)

SuperAdmin-only panel that only displays cost/workload, imposes no limits:

ContentLocationSource
Per-client LLM cost (total + by Specialist)/ops/billing?org=<id>LlmCostRollupService aggregates the unified llm_runs ledger by osa_id (#3194 / PR #3195; the read source switched from messages.cost_cents in #4799)
Platform LLM cost (trend, top clients, by Specialist, by purpose)/ops/billing (SuperAdmin section)PlatformCostStatsService reads llm_runs (#4797) + nightly Langfuse↔DB reconciliation badge (#4798)
Per-Specialist Expert workload (items handled + handle time)/ops/billing?org=<id>resolved queue entries → conversation → OSA (#3196 / PR #3200)
Expert leverage ratio (how many clients / Specialist instances one expert covers)/ops/analyticsexpert_access (ADR-007) (#3197 / PR #3199, merged)

LLM cost lands per call in llm_runs (micro-USD precision; the legacy messages.cost_cents/token_input/token_output/model_name columns continue to be written for history only and are no longer read — #4799) and is aggregated by OSA, but is not billed — it is used for audit/analysis only.

4. Usage Cost Guardrail Rules (key)

Note: "Hard-blocked" = whether the behavior is actually stopped; "Observable" = whether ops can see the underlying signal (even if nothing enforces it). The gap between "cannot be enforced" and "cannot even be seen" is exactly where pricing decisions matter most.

GuardrailRuleHard-blockedObservableDimension
Message rate limitingPer-org org_message_quota, default 1000 messages/hour → HTTP 429 (conversations.service.ts, migration 1714000000007-OrgQuota.ts)✅ Yes✅ Yes — /ops/billing?org=<id> Org Usage shows hourly/daily usageThroughput (anti-abuse, not cost)
Suspension for non-paymentorg → suspended after the payment-failure grace period✅ Yes✅ Yes — /ops/billing shows payment-failure/dunning status (#1433/#1551)Payment status (not cost)
Agent deliverySuccessful Hermes replies deliver directly; legacy confidence/review thresholds are inert❌ No content gate✅ Yes — turn latency, model/tool usage, and delivery outcome are observableDelivery
Per-client LLM cost capNone✅ Yes — per-client/per-Specialist LLM cost is on /ops/billing?org=<id> (#3195)Cost (visible but uncapped)
Optional Expert follow-up costNone (after-delivery observation/correction is not metered, priced, or capped)⚠️ Partial — workload (item count + handle time) is visible (#3200); dollarized hour cost is not visible (#3196)Cost (volume only visible)
Usage-based / overage billingNone✅ Yes — message usage is visible (rate-limit panel), but never billedBilling
Gross-margin guardrail / alertNone enforced (SA-B5 only partially visible)❌ No — cost is visible, but gross margin (cost vs. revenue) is not computed anywhereGross margin

5. Why It Matters (risk & follow-up)

Because pricing is a fixed Specialist monthly fee + unlimited usage, a heavy client's model/tool usage and optional Expert follow-up can rise while the monthly fee stays fixed. Ops needs a gross-margin signal; delivery must not be blocked as a cost-control mechanism. LLM cost and Expert workload are visible (§3), but:

  • Expert hours are still not dollarized (no active review-duration tracking, no per-Expert cost rate) — see #3196.
  • There is no per-client cost cap, no overage billing, no gross-margin alert. SA-B5 (per-client gross margin) is only partially complete; SA-E4 (leverage ratio) has shipped.

To safely commit "fixed monthly fee + unlimited usage" to a contract, the patches needed are: a per-client cost/usage soft cap + alert, an Expert-hour cost model, and a gross-margin dashboard (the remainder of SA-B5).

6. Cost Statistics (planned)

Three-tier (platform / client(org) / Specialist) LLM cost statistics have been scoped in docs/specs/llm-cost-statistics.zh.md (approved 2026-07-23). Its four standing decisions are recorded here as design-of-record:

  1. Langfuse = exploration + cross-validation analysis; the in-house DB = billing-grade numbers (the api/src/llm/ implementation and this spec are authoritative for retention, availability, PII, and data provenance).
  2. Specialist becomes a first-class Langfuse dimension via value-tagging (specialist:<id>, source:agent|api); user_id remains org_id, session_id remains the conversation UUID.
  3. Ledger unification — already switched over (#4799, 2026-07-23): Hermes chat-turn cost is dual-written into llm_runs (purpose='chat-hermes', computed server-side as micro-USD from tokens × price table); all cost reads (rollups + daily budget gates) now come from llm_runs; messages.cost_cents continues to be written only for history. The two-week dual-track observation period was waived by the product owner — the old ledger was never formally used or accepted. A 90-day backfill script api/scripts/backfill-llm-runs-from-messages.ts (run once per environment).
  4. Nightly Langfuse↔DB reconciliation, drift alert (>5% or >$1/day), alert-only, no auto-correction.

6.1 Runbook: Querying LLM Cost via Langfuse (Phase 0 — #4790)

Before the in-product ops view ships (Phase 2, #4797), ops uses self-hosted Langfuse (https://langfuse.hptestingsite.com, project humanwork-dev) to answer cost questions. The following queries have all been verified against Langfuse v3.174.1 (2026-07-23). Auth: HTTP Basic, using the project's pk-lf-…:sk-lf-… key pair (in the Railway api service env, LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY); a browser User-Agent is required (the Cloudflare front for that domain returns 403, error 1010, for bot UAs).

"How much did the platform spend today?" — the daily metrics endpoint (also broken down by model):

GET /api/public/metrics/daily?limit=30

"Which orgs spent the most this week (top-N)?" — the Metrics API, grouped by user (= org id). Note: the Metrics API is GET + a URL-encoded query JSON parameter (POST returns 405):

GET /api/public/metrics?query={"view":"traces","metrics":[{"measure":"totalCost","aggregation":"sum"}],"dimensions":[{"field":"userId"}],"fromTimestamp":"<ISO>","toTimestamp":"<ISO>"}

"How much did a given conversation cost?" — the same query with the dimension swapped to "dimensions":[{"field":"sessionId"}] (session = the platform conversation UUID), or via the UI at Tracing → Sessions.

"How much did a given Specialist cost?"filter (not group) by trace metadata:

"filters":[{"column":"metadata","operator":"=","key":"specialist_id","value":"<specialist uuid>","type":"stringObject"}]

Grouping across specialists in a single query is not possible before #4792 (see the spec's spike conclusion); you can loop the filter over known specialist ids, or wait for value-tagging to ship.

UI dashboard (optional, roughly 10 minutes of manual setup — Langfuse 3.174.1 has no public dashboards API, so it can only be built by hand once): in the project, Dashboards → New dashboard → add three widgets: (1) platform daily cost — view Traces, metric Total cost (sum), no breakdown, day granularity, line chart; (2) cost by org — the same metric, breakdown dimension User, horizontal bar chart, top 10; (3) daily trace count — metric Count, day granularity. Drilling into a session needs no widget — use Tracing → Sessions sorted by Total cost.

Caveats (read before citing any numbers)

  • Before #4793 ships, Langfuse's per-org numbers undercount: API-side LlmService calls (summaries, topics, classification, embeddings, transcripts) have no user_id, so they're invisible in any user/org grouping — only Hermes chat turns are attributed. The userId: null rows in group-by-user results are exactly this unattributed spend.
  • LlmService.stream() is neither metered nor reported (spec OD-2).
  • Expert consultation turns have no cost in the DB ledger (cost_usd = None on the agent side); but their Langfuse trace carries an inferred generation cost — until Phase 2 unification, this is another small source of Langfuse↔DB drift.
  • These are analytics numbers, not billing numbers (decision D1): billing-grade queries should use the DB ledger as the source of truth.

7. Source Map

ConcernFile
Pricing / subscription syncapi/src/billing/application/commands/sync-specialist-subscription.command.ts, .../assignment-billing-input.resolver.ts, infrastructure/providers/lago.provider.ts
Invoice line items / prorationapi/src/billing/application/line-item-calculator.service.ts
Access gate (dunning)api/src/billing/access-gate.service.ts
Payment-failure notificationapi/src/billing/infrastructure/billing-payment-failed.notifier.ts
LLM cost rollupapi/src/billing/llm-cost-rollup.service.ts, .../queries/org-llm-cost.query.ts
Expert workloadapi/src/billing/application/queries/org-expert-workload.query.ts
Expert leverage ratioapi/src/expert-access/expert-leverage.query.ts
Message rate limitingapi/src/conversations/conversations.service.ts, api/migrations/1714000000007-OrgQuota.ts
  • User stories: SA-B5 (per-client cost / gross margin), SA-E4 (Expert leverage ratio) — docs/user-stories.md
  • Status: docs/implementation-status.md
  • Issues/PRs: #3194/#3195 (LLM cost), #3196/#3200 (Expert workload), #3197/#3199 (leverage ratio, merged)