Skip to content
Docs

Governance & Cost Control

AgentWatch’s governance model treats AI spend and AI access as things to be controlled, not merely reported. The distinction matters: a monthly report tells you what already happened, while an enforced budget stops it.

Budgets are set at four tiers — organization, team, employee, and individual user API key — with independent daily and monthly limits at each level, and every tier’s spend is tracked continuously.

Two of those tiers enforce at the gateway, rejecting the request with 402 BUDGET_EXCEEDED rather than letting spend continue:

Tier Tracked Enforced at the gateway
Employee (tenant) Yes Yes, always
Team Yes Yes, when Enforce team budgets is enabled (see below)
Organization Yes Not yet — reporting and alerts only
User API key Yes Reporting and alerts only

Where two enforcing tiers both apply, the strictest wins: the employee’s own limit is evaluated first, then their team’s, and the first one that is exhausted refuses the request. Alongside enforcement:

  • Threshold alerts fire by webhook and email at 80%, 90%, and 100% of each tier’s daily and monthly budget, with configurable recipients per tier.
  • Admin overrides carry an expiry — extra headroom can be granted for a known launch or migration without anyone having to remember to revoke it.
  • Efficiency leaderboards rank teams by tokens-per-dollar and month-over-month spend trend, which is what makes FinOps review of AI usage possible at all.

Every dimension the gateway can see: per model, per user, per team, per organization, per API key, per application, per MCP server, and per individual tool. On breach, the configured behavior is selectable per scope — fall through to the next-priority provider, return a 429 with a retry-after, or raise a webhook or email alert. Concurrency caps, queue depth, and queue timeouts are configurable so a burst is absorbed rather than turned into failures.

Can spend be attributed to individual employees?

Section titled “Can spend be attributed to individual employees?”

Yes, and each employee is a first-class identity rather than a tag on a log line. Per employee, AgentWatch supports:

  • A profile with department, team, role, and status.
  • A scoped API key inheriting the organization’s model access, budget, and DLP policy.
  • Personal daily and monthly budgets applied as a stricter cap than the team’s.
  • Pause and resume of access — for a departure, suspension, or investigation — without destroying the audit trail.
  • Forced key rotation with email notification and an overlap window.
  • CSV bulk import so hundreds of employees can be onboarded from an HR export.
  • Automatic task categorization, classifying each request into Code, Content, Data Analysis, or a custom category — which is how a report can answer “how much of last quarter’s AI spend went to code generation versus marketing copy?”

Every request records tokens, cost, duration, model, provider, team, user, and session, so spend rolls up along any of those dimensions for internal chargeback or showback. Reports export from the UI and programmatically for finance systems, and cached-token counts are surfaced separately so savings from prompt caching are visible rather than hidden in a lower total.

Natively. Each organization is a fully isolated tenant — providers, keys, model catalogs, employees, teams, budgets, DLP policies, audit trails, and analytics are all tenant-scoped at the data layer, with no cross-organization access path. A platform-level super administrator can see across organizations; an organization administrator sees only their own. The hierarchy runs organization → teams → tenants/users → model access and API keys, with per-tenant rate limits, token limits, budgets, suspension, and feature flags.

This isolation model is what makes AgentWatch deployable not just internally but as a governed AI gateway offered to others — by ISVs, managed service providers, or a holding company serving its operating units.

Yes. Administrators provision which models are exposed to which teams and tenants, in bulk if needed, and can alias models per organization for governance naming. A model can be disabled to retire it without breaking applications that still reference it.

Budgets are set per organization, team, employee or API key. Of these, the employee and team tiers are the ones the gateway enforces (team enforcement is opt-in — see the next section); the strictest enforcing limit wins. Set organization defaults first, then override the exceptions.

Console. Budget Management → pick the scope → set a daily and monthly limit. New employees inherit the organization default.

API.

Terminal window
# organization-wide defaults, applied to employees created from now on
curl -s -X POST https://<your-agentwatch-host>/admin/company/defaults/budget \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"defaultDailyBudget": 10, "defaultMonthlyBudget": 300}'
# one employee
curl -s -X POST https://<your-agentwatch-host>/admin/budget/limits/<tenantId> \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"dailyBudgetUSD": 25, "monthlyBudgetUSD": 500}'

Verify. Read the live counters back — this is also what you poll if you want your own alerting:

Terminal window
curl -s https://<your-agentwatch-host>/admin/budget/status/<tenantId> \
-H "Authorization: Bearer <tenant-admin JWT>"

How do I make a team budget actually block?

Section titled “How do I make a team budget actually block?”

Team budgets are tracked for every deployment, but enforcing them is opt-in per organization. That default is deliberate: every team already carries a daily and monthly limit (50 / 1500 by default) that many deployments have only ever used for reporting, so switching enforcement on globally would start refusing traffic against numbers nobody chose. Review your team limits before you turn it on.

Console. Settings → Monitoring & Alerts → tick Enforce team budgets. Review each team’s limits first in Teams → the team → Budget: the dialog shows both the Team Daily Budget ($) and the Team Monthly Budget ($), with today’s team spend. The daily limit is usually the one a busy team reaches first.

API.

Terminal window
# 1. check what the team's limits and spend actually are
curl -s https://<your-agentwatch-host>/admin/api/teams/<teamId>/budget \
-H "Authorization: Bearer <tenant-admin JWT>"
# → {"monthlyBudget":1500,"spent":412.88,"remaining":1087.12,
# "dailyBudget":50,"dailyRemaining":31.40,"warningLevel":"none", ...}
# 2. set them where you want them
curl -s -X POST https://<your-agentwatch-host>/admin/api/teams/<teamId>/budget/limits \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"dailyBudgetUSD": 100, "monthlyBudgetUSD": 2000}'
# 3. then turn enforcement on for the organization
curl -s -X POST "https://<your-agentwatch-host>/admin/company/settings?organizationId=<org id>" \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"teamBudgetEnforcement": true}'

Verify. Settings are cached for 30 seconds, so wait a moment, then send a request as a member of a team you have deliberately capped low. You should see:

{"error":{"code":"BUDGET_EXCEEDED","message":"Budget limit exceeded: Team \"Engineering\" monthly budget exceeded ($2004.10 / $2000.00)"}}

The message names the team, so a member who is comfortably inside their own personal budget is not sent looking at the wrong screen. The rejection is also written to the usage table as a blocked row with blockCode: budget_exceeded and zero cost, so it reports beside guardrail blocks.

This applies to every identity the team contains, including employees created automatically by a service key using delegated identity: an auto-created employee joins the service account’s team, and is governed by that team’s budget from its first request.

Set a token limit. If your contracts are written in tokens rather than dollars, this is the control you want: it refuses with 402 TOKEN_LIMIT_EXCEEDED, deliberately a different code from BUDGET_EXCEEDED, so “out of tokens” and “out of money” stay separate conversations with your own customer. Limits exist daily and monthly, at both the employee and the team tier, and are enforced by the same gate as dollar budgets — employee dollars, then employee tokens, then team dollars, then team tokens; the first one exceeded refuses.

⚠ Nothing is limited until you set a number. Every employee and team starts with no token limit, and 0 also means unlimited — it is not “block everything”.

Console — employee. Budget Management → the Token Limit column. Click it to edit, enter a daily and/or monthly ceiling, and leave a field blank for no limit. The column shows consumption against each ceiling with the same colour thresholds as the spend bars, so someone approaching a limit is visible before they are refused. An employee with no limit shows what they have used and the words no limit.

Console — team. Teams → the team’s Budget dialog → the daily and monthly token ceilings beside the dollar budgets, with a usage bar. Team enforcement follows the same Enforce team budgets switch as team dollar budgets (previous section).

The budget overview carries a token tile: total tokens this month and how many employees a limit actually covers. If it reads No limits set, nothing is being enforced — which is different from nobody being near a limit.

API.

Terminal window
# one employee — daily and monthly ceilings
curl -s -X PUT https://<your-agentwatch-host>/admin/tenants/<tenantId> \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"dailyTokenLimit": 200000, "monthlyTokenLimit": 5000000}'
# one team — dollars and tokens together
curl -s -X POST https://<your-agentwatch-host>/admin/api/teams/<teamId>/budget/limits \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"dailyBudgetUSD": 100, "monthlyBudgetUSD": 2000,
"dailyTokenLimit": 500000, "monthlyTokenLimit": 10000000}'

Send null to remove a limit (an empty field in the console does this for you). ⚠ Before 2.0.272 a cleared limit reported success and stayed in force; if you have ever tried to remove one and found it still applied, clear it once more on 2.0.272 and it will take effect.

Verify. Send a request as someone you have deliberately capped low:

{"error":{"code":"TOKEN_LIMIT_EXCEEDED","message":"Token limit exceeded: monthly token limit exceeded (23,131 / 1,000 tokens)"}}

The message names both numbers. Like a budget block, the rejection is written to the usage table as a blocked row with zero cost, so refusals report beside guardrail blocks rather than existing only in a log.

Symptom Cause Fix
Usage far past the limit, still served 200 No limit is set — the field is blank or 0, both of which mean unlimited Enter a number in Budget Management → Token Limit
A team is over its token ceiling, nothing is refused Team enforcement is off Settings → Monitoring & Alerts → Enforce team budgets
A cleared limit still refuses Fixed in 2.0.272; a clear before that release did not save Clear the field again on 2.0.272
402 TOKEN_LIMIT_EXCEEDED on an account you thought was uncapped A team ceiling, not theirs — the message names the team Raise the team limit, or grant a time-boxed override (next section)

How do I unblock someone who has hit their limit?

Section titled “How do I unblock someone who has hit their limit?”

Grant a time-boxed override rather than raising the limit permanently — it expires by itself, so nobody has to remember to put it back. An override adds its amount to both the daily and the monthly limit until it expires (1–168 hours).

First, find out WHICH limit is refusing them. In Budget Management, an employee refused by their team shows Blocked by team (daily) on their row, with a Team budget button. An employee override does not lift a team limit — use the team override below instead.

Employee override — console. Budget Management → the employee’s row → Override. The form shows how far over they are and pre-fills a suggested amount. ⚠ An override exactly equal to the overage leaves them at $0 remaining, which is still refused — the suggestion adds a margin (10%, at least $1) for that reason.

Terminal window
curl -s -X POST https://<your-agentwatch-host>/admin/budget/override/<tenantId> \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"overrideAmount": 50, "durationHours": 24, "reason": "quarter-end close"}'
# and to revoke it early
curl -s -X DELETE https://<your-agentwatch-host>/admin/budget/override/<tenantId> \
-H "Authorization: Bearer <tenant-admin JWT>"

Team override — console. Teams → the team → Budget → Team Emergency Override (amount, hours, reason; Clear Override removes it). The same dialog has the team’s Daily Budget ($) beside the monthly one.

Terminal window
curl -s -X POST https://<your-agentwatch-host>/admin/api/teams/<teamId>/budget/override \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"overrideAmount": 25, "durationHours": 8, "reason": "launch day"}'
curl -s -X DELETE https://<your-agentwatch-host>/admin/api/teams/<teamId>/budget/override \
-H "Authorization: Bearer <tenant-admin JWT>"

Every override granted or cleared is written to Settings → Audit Logs (budget.override_set / budget.override_cleared, and the team_budget.* equivalents). An administrator can only override employees and teams in their own organization — another organization’s returns 403.

Verify. GET /admin/budget/status/<tenantId> should now show "warningLevel": "normal" (or warning), and its team object "blocked": false:

Terminal window
curl -s https://<your-agentwatch-host>/admin/budget/status/<tenantId> \
-H "Authorization: Bearer <tenant-admin JWT>"
# → {"data":{"dailySpent":3.1,"dailyBudget":60,"warningLevel":"normal", ...,
# "team":{"name":"Support","enforced":true,"blocked":false,
# "dailySpent":48.2,"dailyLimit":75,"blockedPeriods":[],
# "override":{"amount":25,"active":true,"expiry":"..."}}}}
Symptom Cause Fix
Extended the budget but still blocked (1) The team is over its limit — usually the daily $50 default — and team enforcement is on; the employee’s own row looked fine. (2) The override equals the overage exactly, leaving $0 remaining. (3) Before 2.0.363, a request in flight while you saved could re-cache the old “blocked” for 30 seconds. (1) Look for Blocked by team on the row; raise the team’s Daily Budget or apply a Team Emergency Override. (2) Use the suggested amount (overage plus a margin). (3) Upgrade to 2.0.363, or wait 30 seconds. The 402 details.blockers list names every limit refusing the request.
Raised one limit, refused by another More than one limit applies (employee and team, or dollars and tokens) Read details.blockers in the 402 — one entry per limit, with scope, period, limitType, spent and limit
402 PLAN_LIMIT_EXCEEDED after a blocked employee’s tool kept retrying Before 2.0.363 refused requests counted toward the plan’s API-call allowance Upgrade to 2.0.363 — only served requests count now
Override returns 403 The employee or team belongs to another organization Sign in to that organization’s console

Usage Insights shows spend by employee, team, model and provider over any window. For chargeback, pull the same numbers as data:

Terminal window
curl -s "https://<your-agentwatch-host>/admin/budget/report?window=30d" \
-H "Authorization: Bearer <tenant-admin JWT>"
# spend attributed by any custom tag your applications send
curl -s "https://<your-agentwatch-host>/admin/analytics/usage-by-tag?key=cost-center" \
-H "Authorization: Bearer <tenant-admin JWT>"

Tag headers (x-aw-tag-<key>) need no configuration on the client side; register the header names you want captured under Settings → Usage Tags and they become groupable dimensions.

Which calls count toward budgets and token limits?

Section titled “Which calls count toward budgets and token limits?”

Every call AgentWatch serves on its own provider credentials is metered from the token counts the provider reports, and priced from the model catalog. That includes streamed calls: since 2.0.371 the gateway records the provider’s own counts from the stream. For OpenAI-shaped APIs it asks for them with stream_options.include_usage. Pass-through calls are metered too: /v1/responses and native Gemini since 2.0.370, and Cohere /v1/generate and /v1/embed since 2.0.371.

When a provider reports no usage, AgentWatch records an estimate (about four characters per token) so the call still counts, and marks the row tokensEstimated. Anthropic’s legacy /v1/complete is always estimated, because it sends no token counts; use /v1/messages for exact counts.

You see Why What to do
Streamed rows before 2.0.371, or /v1/responses rows before 2.0.370, have different token counts from your provider’s bill Those versions recorded an estimate, or 0 Nothing for new calls. Old rows are not rewritten.
A row is marked tokensEstimated The provider sent no usage for that call Expected for /v1/complete. For a chat stream, check the gateway log for stream-include-usage-refused: that upstream refused to report usage.
A row has tokens but cost 0 and is marked unpriced The model has no price in the catalog Set the input and output price on the model in Model Catalog (console sidebar)
A failed call shows cost 0 The provider refused it, so nothing was served Expected
A /v1/messages row is marked cancelled The client disconnected mid-answer; the tokens already generated are recorded Expected
AgentWatch Connect traffic shows cost 0 The employee’s own account pays the provider, not your gateway keys Expected; tokens are still recorded

Anthropic prompt-cache tokens are not yet added to the recorded input count.

Rate limits are requests per minute, checked for every gateway request before it reaches a provider. Four limits apply, and a request must be inside all of them:

Limit Where to set it Default
Per user — each employee Settings → Rate Limits (perUserRateLimit) 300
One employee’s own limit — replaces per user for that employee Employee Management → Edit → Max Requests/Min, or the Rate Limit column in Budget Management not set
Per API key Settings → Rate Limits (perApiKeyRateLimit, 0 = off) off
Whole organization — all employees together Settings → Rate Limits (globalRateLimit) 1500

An employee’s own limit can be lower than the organization’s per-user limit (to slow one person down) or higher (for a heavy user). Leave it blank or 0 to use the organization’s per-user limit — emptying the box and saving removes an employee’s limit (the same goes for their Monthly Token Limit). The per-API-key and organization limits still apply to that employee.

The same values can be set over the API. Send only the fields you want to change — the response echoes what was saved:

Terminal window
curl -s -X PUT https://<your-agentwatch-host>/admin/alerts/rate-limit/limit \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"perUserRateLimit": 60, "perApiKeyRateLimit": 120, "globalRateLimit": 1000, "rateLimitMode": "enforce"}'

rateLimitMode is enforce (429 over the limit) or monitor (allow, but alarm — useful for a trusted enterprise gateway you do not want to throttle while you find the right number). Hits appear under Rate-limit Alarms. Repeated hits of the same limit by the same employee and key raise one alarm per 5 minutes (AW_RATE_LIMIT_ALERT_THROTTLE_MS), so a client in a retry loop does not flood the list or your inbox; changing the mode or the limit value starts a new alarm straight away.

Terminal window
curl -s https://<your-agentwatch-host>/admin/alerts/rate-limit \
-H "Authorization: Bearer <tenant-admin JWT>"

Testing a limit. A changed limit takes effect within 30 seconds (settings are cached). Test with an employee’s API key — requests made with a super-admin key are never rate-limited. Each request counts once, whichever endpoint it calls. A limit of N per minute refills continuously (one request every 60/N seconds), so after a 429 the next request is allowed as soon as one has refilled, not only at the top of the minute.

Response Meaning Where to look
402 BUDGET_EXCEEDED An employee budget Budget Management → that employee
402 BUDGET_EXCEEDED, message names a team A team budget (enforcement is on) Teams → that team’s limits
402 with several details.blockers More than one limit refuses it — the message names the first, the list names all Each entry’s scope (employee/team), period and limitType
402 PLAN_LIMIT_EXCEEDED The organization’s plan call limit (refused requests do not count toward it from 2.0.363) Platform Admin → Plans
429 A rate limit Rate-limit Alarms names the tenant and key
429 with Retry-After The provider’s rate limit, passed through Provider key pool — add keys or lower concurrency

Budget and plan rejections are written to the usage table as blocked rows with zero tokens and a blockCode (budget_exceeded, plan_limit_exceeded), so they appear in reporting beside guardrail blocks rather than vanishing. Rate-limit rejections are recorded the same way — blockCode: rate_limit_exceeded — when you enable “Record rate-limited (429) requests in the usage table” under Settings → Rate Limits. That one is off by default because a retrying client can generate thousands of 429s a minute, and each one would be a row.

Terminal window
curl -s -X POST "https://<your-agentwatch-host>/admin/company/settings?organizationId=<org id>" \
-H "Authorization: Bearer <tenant-admin JWT>" -H 'Content-Type: application/json' \
-d '{"rateLimitRecordBlockedUsage": true}'

I changed a limit and the next call still went through. Why?

Section titled “I changed a limit and the next call still went through. Why?”

Before 2.0.295, up to half a minute. The new limit reached the database immediately, but enforcement kept reading the previous one from a 30-second authentication cache that the write did not clear — so a lowered limit did not bind until that cache expired.

From 2.0.295 a limit change takes effect on the next request. If you are on an earlier version and need a limit to bind at once, wait 30 seconds before testing, or restart the gateway.

This applies to an employee’s daily and monthly token limits, their rate limit, and team limits saved from the Teams dialog.