Skip to content

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:

controllers/services/entitlements.py
_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 # 100
  • self_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 of plan_tier is 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-led POST /{analyzer}/{namespace}/analyze path only — the client-led workflow is still gated by org_is_entitled on ingest/rules, but doesn’t itself consume from the quota). Not entitled → 402 before 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.pyorg_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.

TierAccessBilling
freeEvery analyzer until the 100-analysis lifetime demo runs outNever billed
paidEvery analyzer, unlimitedEvery resource from the first, degressive
enterpriseEvery analyzer, unlimitedOverage 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_count is 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_resources and a charge, never a 402. Cutting off a paying customer mid-pipeline because a counter moved is a support incident, not a feature.
  • Expiry is advisory. A lapsed expires_at flags 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:

  1. Pre-flight — before any LLM call or job row is created: current month’s spend (get_monthly_llm_cost, summed from usage_events) compared against the cap. Already met/exceeded → 402, nothing started.
  2. Mid-runrun_analysis() takes a budget_remaining_usd param, decremented by each unit’s actual cost; hitting zero mid-run stops the loop early rather than failing the whole request (quota_exceeded: true on 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).