Back to docs
Guide 00

Quotas & costs — usage and spending ceilings

The quota engine covers three kinds: the token quota and the storage quota (Phase 207) plus the cost quota (Phase 216, hold-and-settle over real-money math). Quotas are the operator's tool for usage ceilings in multi-user installs; they bind every user *…

4 min read

The quota engine covers three kinds: the token quota and the storage quota (Phase 207) plus the cost quota (Phase 216, hold-and-settle over real-money math). Quotas are the operator's tool for usage ceilings in multi-user installs; they bind every user with a configured quota — admins included — and an unset quota means unlimited (fail-open: zero behavior change for existing instals).

Community feature (an operator tool). Sources: docs/QUOTAS.md (Phases 207 + 216), docs/MODEL_COSTS.md, docs/API.md (the Costs, quotas, and per-user model policy section). Provenance rows: FACT-CENSUS.md F-QUOTA-1.

Core invariants

  • No second meter. The token/cost consumption truth stays inside the EXISTING WorkspaceTokenUsage ledger (packages/server/src/services/quotaService.ts). QuotaBalance / QuotaHold / QuotaReset are derived state and anchors, never a counter table.
  • Reset = a ledger anchor. A reset writes the QuotaReset(kind, at) anchor row bounding the rolling window — the usage/cost history is never deleted or zeroed.
  • Service-level, pre-turn gates — NOT auth middleware (the check needs domain data, not the request shape). The gates fire before the first LLM call in the orchestrator and before the ingest in the upload paths.
  • Fail-open for unset quotas. The resolution chain ends at "unlimited" when nothing is configured (D-08).
  • Money math is Decimal→cents. ALL cost arithmetic converts through Prisma.Decimal and lands as INTEGER cents — never float (costQuotaService.ts, the COST-01 doctrine).

The resolution chain (D-08)

TierSourceNotes
1The per-user overrideUser.tokenQuotaLimit / User.storageQuotaGb / per-user cost rows — via PUT /api/quota/:userId (admin)
2The install presetSystemConfig keys (below) — DB > ENV > default
3UnlimitedThe "0" / "" / unset sentinel = NOT configured → fail-open

Configuration keys (SystemConfig, admin-editable via Settings)

KeyDefaultMeaning
QUOTA_TOKEN_DEFAULTthe unset sentinelThe install-level default token ceiling for every user without an override
QUOTA_STORAGE_GB_DEFAULTthe unset sentinelThe install-level default storage ceiling (GB)
QUOTA_COST_EUR_DEFAULTthe unset sentinel ("0"/"" = unlimited)The install-level default COST ceiling — an EUR decimal string
QUOTA_COST_FALLBACK_INPUT_CENTS_PER_1Ma non-zero built-in floorThe SC-4 fallback rate for UNPRICED models — input, integer cents per 1M tokens
QUOTA_COST_FALLBACK_OUTPUT_CENTS_PER_1Ma non-zero built-in floorThe SC-4 fallback rate for UNPRICED models — output

The SC-4 doctrine: a model without pricing NEVER bypasses the cost quota — the fallback rates apply with a warn-once log (warnFallbackOnce), so an unpriced model can never appear free.

Token quotas (quotaService.ts)

  • resolveTokenQuota(userId) → { limit, source } — limit: null means unlimited; source names the chain tier that decided.
  • The rolling window starts at the latest QuotaReset anchor for (userId, kind: "token") (or the epoch when never reset); the windowed usage sums from the WorkspaceTokenUsage ledger.
  • Exceeding the quota raises QuotaError (status + payload) — the payload's quota field is the contract the frontend, the widget, and the connector handlers key on. There is no middleware to bypass — the gate runs inside the service path.

Cost quotas (costQuotaService.ts, Phase 216) — hold-and-settle

A two-phase reservation:

  1. Reserve (before the LLM call) — reserveCostQuota(principal, estimateCents) runs ONE interactive transaction: a conditional updateMany whose predicate (spentCents + estimate <= limitCents) Postgres evaluates row-locked, so N concurrent gates SERIALIZE instead of all passing a stale check (SC-2) + the QuotaHold insert in the same transaction.
  2. Settle (at the usage seam) — settleCostHold finalizes the hold and refunds estimate − actual. The in-flight overshoot (actual > estimate) is the accepted window — no mid-stream kill.
  3. At rest the invariant holds: settledSpent + Σ(open holds) ≤ limit.
  4. The pricing source per row: the FROZEN Phase 203 snapshot where one exists; otherwise the SC-4 fallback rates (warn-once); rows in a currency outside the ledger's dominant one are still priced (never silently free, never excluded).
  5. Reset — resetCostQuota writes the Quota(kind "cost") anchor and REBUILDS the balance from the ledger sum + the open holds (D-02).

Storage quotas

checkStorageQuota / resolveStorageQuota enforce the per-user storageQuotaGb override, the QUOTA_STORAGE_GB_DEFAULT preset, or unlimited. The gates fire in the upload paths before dispatching to the collector.

The widget quota principal

Anonymous widget visitors are not users — resolveWidgetQuotaPrincipal binds widget traffic to the widget's organization principal: widget consumption counts against the widget org's quota chain rather than a per-visitor identity.

Where the gates fire (the consumption sites)

SurfaceGateFile
Chat (JWT, widget proxy, connectors — one orchestrator)the token + cost gates pre-turn / pre-LLMpackages/server/src/agent/orchestrator.ts
Document uploadthe storage gate pre-ingestpackages/server/src/routes/uploads.ts, routes/documents.ts
The reset sweepthe hourly token-quota reset (the pg-boss cron: initQuotaResetScheduler)services/quotaResetJob.ts
Self-service readsGET /api/quota/me, rollups, manual resetroutes/quota.ts

The operator workflow

  1. Set the install defaults — the Settings UI → the QUOTA_* keys (or PUT /api/system/settings).
  2. The per-user overrides — PUT /api/quota/:userId (admin) or the quota UI.
  3. The per-user pricing — the COSTS family (Phase 209: /api/users/:userId/pricing, the own-provider keys under /api/me/provider-keys) feeds the same cost engine.
  4. A manual reset any time: POST /api/quota/:userId/reset; the hourly sweep handles the steady state.

Model pricing (MODEL_COSTS)

Setup: Settings → Providers → pick the provider → the model's pricing action → the per-1M-token input/output cost (e.g. 1.50 and 6.00) + the currency → Save. The formula: cost = tokens × rate, computed server-side on every token-usage write, snapshot-frozen at run time (later rate edits never rewrite history). Currencies: USD, EUR, GBP, JPY, CNY, INR — per-currency totals, no conversion in v1. N/A = pricing not configured (unset cloud models; the UI shows "N/A" — the admin should configure the rate); $0.00 = explicit free (the local/Ollama models are backfilled to 0 at migration; overridable). v1 limitations: no cross-currency conversion; the main-iteration cost only (the tool/MCP/synthesis sub-pipeline costs excluded); the cost lands at the done event (not mid-stream).

The endpoint reference

The complete endpoint table lives in API reference → Costs, quotas, and per-user model policy.

See also

This guide covers the operator surface for the quotas and costs; the engineering deep dive lives in the twin post:

Provenance

SectionSource
Invariants / the resolution chain / the keysdocs/QUOTAS.md (the core invariants, D-08, the keys — Phase 207/203) — F-QUOTA-1
Cost quotas / hold-and-settledocs/QUOTAS.md (§ Cost quotas — Phase 216) + costQuotaService.ts
Model pricingdocs/MODEL_COSTS.md (Phases 203/209/216)
The gate sitesdocs/QUOTAS.md (§ Where the gates fire)

---

Operator reference — Simmetric Chat v0.28.1 (docs refreshed 2026-10-02, census SHA `aca5b8ff`).