Billing & Quota Enforcement
Two independent mechanisms, gating different things:
Deployment-mode entitlement — can this org run an analyzer at all
Section titled “Deployment-mode entitlement — can this org run an analyzer at all”config.deployment.mode (Literal["self_hosted", "cloud"], file/env-only, never
admin-API-editable — see Platform settings)
identifies which kind of deployment this process is, not a per-org setting:
_PAYING_TIERS = frozenset({"paid", "enterprise"})
def org_is_entitled(org: dict, deployment_mode: str) -> bool: if deployment_mode == "self_hosted": return True if org.get("plan_tier") in _PAYING_TIERS: return True return org.get("demo_analyses_used", 0) < FREE_DEMO_ANALYSES # 100self_hosted(the default for every OSS/quickstart/Helm config) — every org is entitled, unconditionally. There is no per-analyzer free/paid split; the whole notion ofplan_tieris inert here.cloud(set only on the operator’s own SaaS config) — an org needs a paying plan, or is still within its one-time, lifetime 100-analysis free demo (demo_analyses_used, incremented by the server-ledPOST /{analyzer}/{namespace}/analyzepath only — the client-led workflow is still gated byorg_is_entitledon ingest/rules, but doesn’t itself consume from the quota). Not entitled →402before ingest/rules/analyze does anything.
Why nothing outside this module names a tier
Section titled “Why nothing outside this module names a tier”Three separate questions used to be answered by comparing plan_tier to the literal "paid", in four places across two repos: may this org analyse?, does this call burn demo credit?, and does this org get a dollar figure? Those answers agreed exactly as long as there were two tiers — and every one of them inverts the moment a third exists. An "enterprise" org would have burned demo credit, been 402’d at 100 analyses, and shown no cost.
Each question now has one name and one implementation in entitlements.py — org_is_entitled, consumes_demo_quota, is_metered, plus billable_resource_count/active_contract/contract_expired for commitments. Adding a fourth tier means editing one module, not grepping for a string.
| Tier | Access | Billing |
|---|---|---|
free | Every analyzer until the 100-analysis lifetime demo runs out | Never billed |
paid | Every analyzer, unlimited | Every resource from the first, degressive |
enterprise | Every analyzer, unlimited | Overage only, above a committed monthly allowance, flat at the cheapest tier |
An enterprise org carries an OrgContract (included_resources_per_month, committed_amount_usd, expires_at, reference) set by a superadmin at signature time. Three properties are deliberate:
- The meter keeps running.
resource_countis recorded unconditionally for every org on every tier, so a commitment is a billing basis, not an off switch — the operator keeps full cost visibility, and overage is computable for free. - The ceiling is soft. Exceeding it produces
overage_resourcesand a charge, never a402. Cutting off a paying customer mid-pipeline because a counter moved is a support incident, not a feature. - Expiry is advisory. A lapsed
expires_atflags the org for renewal in the admin list and on its usage page. It revokes nothing.
An empty contract means unlimited, not zero: an operator who set the tier without recording terms has granted access, and failing closed there would cut off a customer over missing paperwork.
The contract is preserved across a downgrade so re-upgrading doesn’t lose it, which means a stored contract is not itself proof any terms apply — every reader goes through active_contract(), which returns it only while the org is on the tier it governs.
Per-org spend cap — how much an entitled org can spend
Section titled “Per-org spend cap — how much an entitled org can spend”An optional per-org monthly LLM spend cap (organizations.quota.monthly_budget_usd, default unlimited) is enforced at two points around every LLM-cost-incurring path — server-side /analyze and the GitHub webhook job alike:
- Pre-flight — before any LLM call or job row is created: current month’s spend (
get_monthly_llm_cost, summed fromusage_events) compared against the cap. Already met/exceeded →402, nothing started. - Mid-run —
run_analysis()takes abudget_remaining_usdparam, decremented by each unit’s actual cost; hitting zero mid-run stops the loop early rather than failing the whole request (quota_exceeded: trueon the response/job, not an error).
controllers/billing.py computes the degressive per-resource pricing used once usage is metered; routers/pricing.py exposes it (GET /pricing).