Budgets & caps
Four-layer credit-spend caps stack across org → application → member → user-global. Most-restrictive-wins. Every decision is audited; rollouts run in shadow before enforcing.
The four layers
Every /v1/* request runs a pre-flight gate that walks four caps. Any one trips → 402.
1. Organization (org-wide hard ceiling)
Organization.dailyBudget,Organization.monthlyBudget.- Set in /settings/billing by the org owner. Always blocking; can't be soft-warned.
2. Application (per-project tenant)
dailySpendCap,weeklySpendCap,monthlySpendCap,totalSpendCap.onCapHit=block|notify-only|soft-warn.- Each downstream product (e.g.
fedoverwatch,sup.video) is one Application. Keys minted withapplicationIdbill against its caps. - Optional
alertWebhookUrl+ HMAC secret for cap-hit pings.
3. Member (per-person within org)
- Same four windows on
OrgMember. - Set per-member by org admin via
PATCH /api/orgs/:orgId/members/:userId/budget.
4. User (global personal cap, BUDGET)
User.perUserDailyCredits: applies across every org the user is in. Cross-org safety net.
Cap-hit response
HTTP/1.1 402 Payment Required
{
"error": "Budget cap reached: application_monthly_cap",
"code": "budget-cap-hit",
"capLayer": "application",
"capWindow": "monthly",
"resetsAt": "2026-06-01T00:00:00.000Z"
}The capLayer + capWindow tell you exactly which cap fired. resetsAt is the UTC start of the next window (omitted for total lifetime caps).
Behaviors
block: refuse the call, return 402, emitBudgetEvent.decision = 'refuse'.notify-only: allow the call (200), emitBudgetEvent.decision = 'allow_near_cap', optionally fire the alert webhook. Customer-facing response is unchanged.soft-warn: allow + setX-Genie-Budget-Remainingresponse header so callers can self-throttle.
Shadow rollout. Set
GENIE_BUDGET_MODE=shadow in env to AUDIT every decision without enforcing. BudgetEvent rows accumulate; nothing returns 402. Watch /admin#budget for refusal-rate-over-time, then flip to on.Audit
Every decision (allow / allow_near_cap / refuse) writes a BudgetEvent row with the orgId, userId, reason, mode, estimatedCredits, and a context blob. Same audit trail BUDGET already powered, widened to include the newapplication_* + member_* reasons.
Setting caps via API
# Application monthly cap
curl -X PATCH https://api.genie.tech/api/orgs/{orgId}/applications/{appId} \
-H "Content-Type: application/json" \
-d '{"monthlySpendCap": 1000, "onCapHit": "block"}'
# Per-member cap
curl -X PATCH https://api.genie.tech/api/orgs/{orgId}/members/{userId}/budget \
-H "Content-Type: application/json" \
-d '{"dailySpendCap": 50, "monthlySpendCap": 500}'