Skip to content

Budgets

A budget is a per-agent policy: caps on dollars, tokens and runs per day, plus per-run and per-tool limits. With hardFail: true, a call that would start while a limit is already reached is refused before the model is called: HTTP 402 on the REST paths, a failed step inside workflows. With false, the breach is logged and the call proceeds. If the budget cannot be read, hard-fail budgets fail closed.

Before the model call, the budget gate reads the agent's recorded usage for the day. Under every cap, the call goes to the model. At or over a hard-fail cap, the caller gets HTTP 402 and no provider is contacted.Before the model call, the budget gate reads the agent's recorded usage for the day. Under every cap, the call goes to the model. At or over a hard-fail cap, the caller gets HTTP 402 and no provider is contacted.
PUT /v1/agents/{name}/budget
Content-Type: application/json
{
"maxCostUsdPerDay": 5.00,
"maxCostUsdPerRun": 1.00,
"maxTokensPerDay": 500000,
"maxRunsPerDay": 100,
"toolLimits": { "web_search": 50, "slack.post_message": 100 },
"hardFail": true,
"notifyAtPct": 80
}
Field Meaning
maxCostUsdPerDay Max USD across all runs in one budget day. Blocks once the day’s recorded spend exceeds it
maxCostUsdPerRun Max USD for one run. Enforced mid-run for workflow runs only (below). Elsewhere it is checked only by the opt-in forecast and reported after the fact as an anomaly_detected event
maxTokensPerDay Max tokens (in + out) per day. Blocks once the day’s recorded tokens reach it
maxRunsPerDay Max runs per day. A workflow run counts once, when it succeeds, like a plain run. Failed runs are not counted; spend inside a run does not add to the count
toolLimits Per-tool call counts per day. Keys are a tool name (web_search) or connector.action for connector nodes. Enforced for workflow tool and connector nodes only
hardFail true: a reached limit blocks. false: logged, call proceeds
notifyAtPct Stored (default 80 when omitted or outside 1-100). No alert is sent on it today

Day boundary. The daily rollup resets at local midnight using PAL_DEFAULT_TIMEZONE (an IANA zone; UTC when unset). A schedule’s own timezone affects only when its cron fires, not the budget day.

Read and remove:

GET /v1/agents/{name}/budget # 404 {"error":"no budget configured"} when none
GET /v1/budgets # bare array of per-agent budgets
DELETE /v1/agents/{name}/budget

One daily cap across all agents, including agentless /v1/completions calls: PUT, GET and DELETE on /v1/budgets/tenant with { "maxCostUsdPerDay": 50, "hardFail": true } (must be greater than 0). It is stored under the reserved agent name *, hidden from GET /v1/budgets, and the per-agent routes reject * as a name. A soft tenant cap never masks an agent’s own hard-fail limit.

Each check runs before the model or tool call. A hard-fail budget blocks; a soft budget logs and proceeds.

Path Cost/day Tokens/day Runs/day Tenant cap Per-run Per-tool
Run create, session message, /v1/completions with agentName Yes Yes Yes Yes No No
/v1/completions without agentName n/a n/a n/a Yes n/a n/a
Workflow ai-step and confidence votes Yes Yes No (run already admitted) Yes Yes (spend so far) n/a
Workflow tool, web_search Yes Yes No Yes Yes (spend so far) Yes
Workflow tool, sandbox tools No No No No No Yes
Workflow connector No No No No No Yes (connector.action)
Voice (Twilio inbound, LiveKit token) Yes Yes Yes Yes Flat estimate No
  • web_search inside a session turn is covered by the check at the start of the turn; its spend is recorded after.
  • Voice reserves a flat estimate on connect and reconciles it to the real call duration on the provider’s status callback.
  • Marketplace invokes check the seller agent’s budget and return 402 with seller agent budget limit reached: <reason>.

Workflow runs record each model call’s tokens and cost onto the run as they happen. Before every ai-step, confidence vote and web_search node, the interpreter re-checks the tenant cap, the agent’s daily caps and maxCostUsdPerRun against this run’s spend so far. Hard-fail stops the run at that step with no further provider spend.

toolLimits is checked before a workflow tool or connector node dispatches, and one use is counted after dispatch. A tool that was never dispatched (runtime not wired) is not counted. See Agents.

POST /v1/runs with a reached hard-fail budget. The reason names the limit that tripped:

HTTP 402
{
"error": "agent budget limit reached: daily run limit reached (100/100)",
"reason": "daily run limit reached (100/100)"
}

Other reason strings: daily spend would reach $5.0123 exceeding limit $5.0000, daily token limit reached (500000/500000), tenant-wide daily spend would reach $50.0100 exceeding tenant limit $50.0000, run spend $1.0042 reached per-run limit $1.0000, tool "web_search" budget exhausted (50/50 today).

Caller What it sees
POST /v1/completions 402 { "error": "budget limit exceeded", "reason": "<reason>" }
Session message No HTTP error. The session replies sorry — this agent has reached its usage limit for today and goes idle
Workflow step Fails with budget limit exceeded: <reason>
Voice Twilio <Reject reason="busy"/>; other providers 402 agent voice budget exhausted; the LiveKit token route 402 { "error": "agent voice budget exhausted: <reason>" }

A database error while reading a budget is not “no budget configured” (that allows the call). The check fails closed when the budget is hard-fail, or when the row itself could not be read (its policy is unknown, so it is treated as hard-fail). The error text is logged, never returned.

  • Run create answers 503 with { "error": "budget check unavailable; retry shortly", "reason": "budget check unavailable" }. Only run create returns 503.
  • /v1/completions answers 402 with reason budget check unavailable. Sessions reply sorry — I can't check this agent's usage limit right now, try again shortly. Workflow steps fail with budget limit exceeded: budget check unavailable. Voice declines as for a budget block.
  • Soft budgets: if the row was read and says hardFail: false but the usage lookup then errors, the call proceeds.
Terminal window
TOKEN=$(curl -s localhost:8080/auth/login -H 'Content-Type: application/json' \
-d '{"email":"admin@pal.local","password":"pal"}' | jq -r .token) # dev seed user
# Cap an agent at 1 run per day, hard-fail
curl -s -X PUT localhost:8080/v1/agents/demo/budget \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"maxRunsPerDay":1,"hardFail":true}'
# Create two runs; the second returns HTTP 402 once the first is recorded
curl -s -i -X POST localhost:8080/v1/runs \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"agentName":"demo","input":{"q":"hi"}}'

Usage is recorded when a run finishes, so create the second run after the first completes.

POST /v1/runs/forecast estimates tokens and cost from the agent’s succeeded runs over the last 30 days (average tokens per run, plus about one token per four characters of input) and checks the estimate against the budget. It answers 402 instead of 200 when a hard-fail budget would be breached. Forecasting is opt-in: run creation does not call it.

  • With history: calibrated: true, and the per-run and daily cost caps are checked against the estimate.
  • With no succeeded runs: estimates are omitted, calibrated is false, confidence is 0, and only maxRunsPerDay can block. An unknown cost never auto-blocks.
  • confidence grows with sample size: min(0.95, 0.3 + 0.03 × √runs).
POST /v1/runs/forecast
{ "agentName": "research-agent", "input": "quantum computing" }
200 OK (values illustrative)
{
"agentName": "research-agent",
"estimatedTokensIn": 800,
"estimatedTokensOut": 2000,
"estimatedCostUsd": 0.004,
"confidence": 0.45,
"calibrated": true,
"wouldExceedBudget": false
}

When it would exceed (HTTP 402 if hardFail), wouldExceedBudget is true and blockReason explains, for example per-run cost $1.2000 exceeds limit $1.0000; several reasons join with ; . Others: daily cost would hit $5.0400 exceeding $5.0000, daily run limit reached (100/100). blockReason is omitted when nothing would block.

  • Per-run cap mid-run on single-call paths. A plain run, a session turn and a completion make one model call, so there is no point to stop at. Overshoot is reported after the run as an anomaly_detected event, which blocks nothing.
  • Projected spend at run creation. Run create checks recorded spend, so the run that crosses a daily cap is allowed and the next is blocked. Use the forecast to estimate ahead.
  • Failed runs toward runs/day. Only succeeded runs count. A failing agent never trips maxRunsPerDay, though its spend still counts toward cost and token caps.
  • Per-tool limits outside workflows. Session tool loops and POST /v1/connectors/{id}/execute do not consult toolLimits.
  • Daily cost and token caps on sandbox tool and connector nodes. Those nodes check only toolLimits; their ai-step neighbours carry the model spend and are checked.
  • Reservation. Checks read recorded usage and spend is recorded after the call, so concurrent calls can together overshoot a cap.
  • notifyAtPct alerts. Stored; nothing fires on it.

Each counter is emitted under its pal. name.

Metric Attributes Meaning
pal.run.budget.blocks pal.agent_name Runs refused by a hard-fail budget at POST /v1/runs (HTTP 402)
pal.budget.check.errors pal.budget_check (tenant, agent, run, tool), pal.agent_name Budget lookups that errored and failed closed