Cost budgets & FinOps
Cap LLM spend per API key, per team, per run, per project, and per org. Caps are enforced in the gateway proxy: when a key is over budget the proxy returns 402 Payment Required before the upstream provider is ever called.
Spend is metered from real gateway traffic and estimated per-model, so the counters you cap against are the same numbers the cost dashboards report.
What ships today
Per-key, per-team, and per-run caps are enforced live in the gateway proxy (budget-enforcer.ts) — over-cap traffic is blocked with a 402. Configure them from the API or the evalguard budget CLI. The org-level budget and the effective-cascade resolvership as configuration + read-only resolution today; wiring those two levels into the hot path is a deliberately separate soft→hard rollout.
The levels
Caps are set independently at each level. Under enforcement the resolver applies tightest-cap-wins: the level with the least remaining headroom binds, and any exceeded hard cap blocks the request.
org org_budgets.monthly_budget_usd (config + read-only cascade) project project_settings key=cost_budget (monthlyLimit + thresholds) team team_budgets(org_id, team_id) (enforced in gateway) key api_keys.monthly_budget_usd (enforced in gateway) run agent_runs.per_run_budget_usd (enforced in gateway)
A "team" is the same identifier the rest of the Cost axis attributes spend by — api_keys.agent_tag (mirrored onto each run). A key/run with no agent_tag, or no team_budgets row, simply falls through to per-key behavior — zero regression.
Set a per-key spend cap
The per-key budget is the workhorse: hand different services or tenants different caps under one org account. Set it from the CLI or the REST endpoint. Both are admin-only — spend and caps are billing data.
# Set a $100 cap that resets monthly (the default cadence) evalguard budget set <keyId> 100.00 # Same cap, but reset the counter every day (UTC-aligned) evalguard budget set <keyId> 100.00 --reset-period daily # Change only the reset cadence: daily | weekly | monthly evalguard budget period <keyId> weekly # Inspect current spend + cap + percent-used evalguard budget get <keyId> # # my-service-key # Key 6f1c…-…-… # Cap $100.00 # Cadence monthly # Spent $37.4200 # Remaining $62.5800 # Usage 37.4% # Period 2026-07-01T00:00:00.000Z # Preview then remove the cap (key becomes uncapped/unlimited) evalguard budget clear <keyId> --dry-run evalguard budget clear <keyId>
Setting 0 blocks all gateway traffic on the key. The hard ceiling is $1,000,000 per period to catch typos.
# PATCH sets or updates the cap and/or the reset cadence.
# monthlyBudgetUsd: number sets it, null removes it (unlimited).
curl -X PATCH https://evalguard.ai/api/v1/api-keys/<keyId>/budget \
-H "Authorization: Bearer $EVALGUARD_API_KEY" \
-H "content-type: application/json" \
-d '{ "monthlyBudgetUsd": 100.00, "resetPeriod": "monthly" }'
# GET returns the live budget state:
# {
# "monthlyBudgetUsd": 100,
# "resetPeriod": "monthly",
# "currentPeriodSpentUsd": 37.42,
# "currentPeriodStartedAt": "2026-07-01T00:00:00.000Z",
# "remainingUsd": 62.58,
# "percentUsed": 37.42,
# "staleReset": false
# }
# DELETE is a convenience alias for PATCH { "monthlyBudgetUsd": null }.How enforcement works
Enforcement lives in the gateway proxy, not the config API. On each request checkBudget()reads the key's cap and period-to-date spend before forwarding upstream; after a successful call recordSpend()atomically increments the counter via a Postgres RPC (read + compare + write in one transaction, so concurrent requests don't race).
HTTP/1.1 402 Payment Required x-evalguard-budget-usd: 100.00 x-evalguard-spent-usd: 100.41 # reason codes surfaced by checkBudget(): # exceeded per-key hard cap hit # team_exceeded team HARD cap hit (blocks even if the key is under) # no_cap no per-key cap set (team cap may still block)
Deliberate design choices
- Fail-open on DB errors. A flaky Supabase returns
allowed: true— an accounting hiccup must never black-hole all gateway traffic. - No cron for resets. The period counter resets amortized on the first request of each new period (daily/weekly/monthly, UTC-aligned) — the stale counter reads as
0for enforcement andrecordSpendwrites the new period start. - Per-run caps too.
agent_runs.per_run_budget_usdflips a run tostatus='budget_exceeded'when an increment pushes it over, and the gateway402s subsequent calls on that run.
Org & team budgets and the effective cascade
The org budget is CRUD at the top of the cascade. Setting the row is the opt-in; deleting it disables org enforcement. It carries an enforcementMode (hard blocks, soft only warns). Writes are admin-only and audited.
# Set the org ceiling (admin-only, audit-logged)
curl -X PUT https://evalguard.ai/api/v1/budgets/org \
-H "Authorization: Bearer $EVALGUARD_API_KEY" \
-H "content-type: application/json" \
-d '{ "orgId": "<orgId>", "monthlyBudgetUsd": 5000, "enforcementMode": "hard" }'
# Resolve the effective cascade for a context (READ-ONLY).
# Applies tightest-cap-wins across the levels that track spend and
# reports the binding cap + per-level breakdown + whether a HARD cap
# WOULD block — before the soft→hard rollout changes anything.
curl "https://evalguard.ai/api/v1/budgets/effective?orgId=<orgId>&teamId=<teamId>" \
-H "Authorization: Bearer $EVALGUARD_API_KEY"
# → { "binding": {...}, "levels": [...], "blocked": false, "blockingLevels": [] }Honest about scope: /budgets/org and /budgets/effective are configuration and read-only resolution. They surface which cap binds so operators can preview impact; they do not yet change what the gateway enforces. Per-key, per-team, and per-run caps are the levels enforced live today.
Project budgets & alerts
Project budgets are evaluation-oriented: a monthly limit plus warning and critical thresholds, stored in project_settings. A check action runs the BudgetEnforceragainst the current month's eval_runs spend and returns an allow/deny decision (respecting mode: soft | hard).
# Save a project budget: $500/mo, warn at 80%, critical at 95%
curl -X POST https://evalguard.ai/api/v1/cost/budget \
-H "Authorization: Bearer $EVALGUARD_API_KEY" -H "content-type: application/json" \
-d '{ "projectId": "<id>", "monthlyLimit": 500,
"warningThreshold": 0.8, "criticalThreshold": 0.95, "mode": "soft" }'
# Ask whether a projected cost is within budget before running
curl -X POST https://evalguard.ai/api/v1/cost/budget \
-H "Authorization: Bearer $EVALGUARD_API_KEY" -H "content-type: application/json" \
-d '{ "projectId": "<id>", "action": "check", "estimatedCost": 12.50 }'
# Alert status → level: info | warning | critical | exceeded
curl "https://evalguard.ai/api/v1/cost/alerts?projectId=<id>" \
-H "Authorization: Bearer $EVALGUARD_API_KEY"
# Configure (and test) Slack / email alert channels
curl -X POST https://evalguard.ai/api/v1/cost/alerts \
-H "Authorization: Bearer $EVALGUARD_API_KEY" -H "content-type: application/json" \
-d '{ "projectId": "<id>", "slackWebhook": "https://hooks.slack.com/…",
"notifyEmail": "finops@acme.com" }'When the alert level reaches warning or above, EvalGuard fires the configured Slack/email channels via the notification bridge (fire-and-forget, so it never blocks the response). POST with action: "test" to verify a channel is wired correctly.
Cost tracking & FinOps exports
Budgets cap spend; the cost APIs explain it. These are read-side analytics over the same metered spend the caps enforce against:
GET /api/v1/cost/breakdown ?orgId=…&groupBy=team|cost_center|owner|environment|user
multi-dimensional tag breakdown over cost_entries
GET /api/v1/cost/burn-rate ?orgId=…&windowSeconds=300
live $/hr projection from recent gateway_proxy_logs
GET /api/v1/cost/forecast ?projectId=…&daysAhead=30 projected spend
GET /api/v1/cost/export ?format=focus|openmeter|lago FinOps interchangeThe export connectors emit row-level LLM spend in the standard formats a FinOps team already ingests — FinOps Foundation FOCUS 1.0 CSV, OpenMeter CloudEvents, or Lago usage events — so spend lands in the same data lake as your cloud bill. Pull them on the terminal:
# Row-level FOCUS 1.0 CSV to stdout evalguard cost-export <orgId> --format focus # Lago NDJSON for a date range, written to a file evalguard cost-export <orgId> --format lago \ --start 2026-05-01 --end 2026-05-31 --out spend.ndjson
Roadmap, stated plainly
- Enforced today: per-key, per-team, and per-run caps in the gateway proxy (hard block +
402; soft caps warn). - Config + read-only today: org budgets and the effective-cascade resolver. They preview which cap binds but don't yet gate the hot path — that's the pending soft→hard rollout.
- Opt-in, off by default: budget-triggered model downgrade (
budget_downgrade_enabled) transparently drops to a cheaper model near cap exhaustion and logs the decision to the FinOps dashboard.