Budgets
Multi-scope enrichment spend caps — tenant, team, provider, user, type, and level — with threshold alerts, throttle, and hard-stop auto-suspend.
Multi-scope spend control
Enrichment budgets cap how much a tenant can spend on data enrichment over a
period. Unlike a single account-wide limit, a Pact budget can be scoped to any of
six dimensions (core.enrichment.budgets.VALID_SCOPES):
tenant · team · provider · user · enrichment_type · level
Every charge is checked against every budget whose scope it falls into, and
the most-restrictive remaining headroom wins. Budgets are stored in the
enrichment_budgets table (alembic 0049).
Periods and behaviors
- Period —
day,week, ormonth. Spend resets automatically at the next period boundary (reset_period_if_needed). - Behavior — how the budget reacts when spend meets the cap:
alert— warn only, never block.throttle— block bulk runs and scheduled sweeps at the cap, but still let a single manual lookup through.block— hard-stop every charge at the cap.
The legacy hard_stop boolean is kept coherent with behavior for older rows —
block/throttle imply hard-stop, alert does not.
Threshold alerts and auto-suspend
Each budget carries alert thresholds (default [50, 75, 90, 100] percent). The
first time spend crosses a threshold in a period, an alert fires once — tracked in
alerts_emitted_json so you're not re-alerted on every call. In production,
emit_alerts_and_suspend delivers to email (Resend) and your webhook subscribers.
When a block/throttle budget with auto_suspend reaches 100%, suspended_at
is stamped and subsequent charges are rejected until the next period start.
Enforcement path
check_budget is the read-only predicate; enforce_and_alert is the one-stop
pre-flight that checks, fires threshold alerts, and raises BudgetExceededError
(surfaced as 402 Payment Required) when the charge is denied. A run preview
reports will_block and blocking_scope so the UI can warn before you commit.
Legacy tenant cap still applies underneath
Below the multi-scope budgets sits the original per-tenant cap resolved by
effective_budget_usd_cents: tenants.limits_json["enrichment_budget_usd_cents"]
→ QUOTA_DEFAULT_ENRICHMENT_BUDGET_USD_CENTS env → 0 (unlimited). Any active
scope="tenant" budget overrides it; other scopes stack on top.
Managing budgets
| Endpoint | Purpose |
|---|---|
GET /v1/enrichment/budgets | List active budgets with spend-to-date |
POST /v1/enrichment/budgets | Create a budget |
PATCH /v1/enrichment/budgets/{id} | Update cap, thresholds, behavior, active |
DELETE /v1/enrichment/budgets/{id} | Soft-delete (sets active=false) |
GET /v1/enrichment/budgets/overview | Dashboard: budgets + daily spend series + top spenders |
GET /v1/enrichment/estimate | Pre-lookup cost + your remaining headroom |
curl -X POST https://api.pact.place/v1/enrichment/budgets \
-H "Authorization: Bearer $PACT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q3 enrichment cap",
"scope": "tenant",
"period": "month",
"cap_cents": 500000,
"behavior": "throttle",
"alert_threshold_pcts": [50, 80, 100]
}'
The GET /v1/enrichment/estimate route powers the inline "this lookup will cost
~$0.12" confirmation on the enrich buttons — the headroom returned reflects
your scopes, not just the tenant total.