Usage and spend caps
What was spent, and how to stop it being spent.
Every turn writes a session.usage event, and those are folded into rollups you can
query. usage.* costs only read, so a client can render its own budget without
holding control.
const period = { from: "2026-09-01", to: "2026-09-30" }
// The environment, with each cap beside its meter.
const usage = await client.usage.environment({ period, groupBy: "agent" })
usage.totals.turns
usage.totals.costUsd
usage.spend.tokensPerMonth
// Per key, with each key's own cap.
await client.usage.keys({ period })
// One session, and its sub-agents.
const tree = await client.usage.session({ id: sessionId })
tree.own
tree.descendantUsagecostUsd is a lower bound whenever unpricedCalls > 0. A model Plane's price
table does not know contributes nothing rather than a guess. Cost is computed per
model call, not from a turn's summed tokens: a turn that fell down a fallback ladder
used two models at two prices.
Caps
await client.environments.update({
slug: "dev",
spend: { costUsdPerMonth: 50, tokensPerMonth: 20_000_000 }
})
await client.apiKeys.update({ id: keyId, spend: { costUsdPerMonth: 10 } })A key's cap may not exceed its environment's (SpendCapExceedsParentError).
Caps are checked in exactly three places — sessions.create, sessions.send, and
when an agent spawns a sub-agent — and nowhere else.
- A running turn always finishes. Cutting one mid-flight would leave an open tool call and a half-written answer to save a few cents.
sessions.decideis never gated. Refusing an approval would strand a session inrequires_actionforever, which is worse than one turn of overage.- An environment with no caps never reads the rollup at all, so the check is free for everybody who has not asked for it.
try {
await client.sessions.send({ id, message })
} catch (error) {
if (error instanceof SpendLimitExceededError) {
// `resetsAt` is the only thing that changes the answer. Retrying before it is a
// retry that cannot succeed.
console.warn(`${error.scope} out of ${error.metric} until ${error.resetsAt}`)
}
}It is a 402, not a 429. 429 means "slow down", and every client library retries it with backoff; a monthly cap is not a rate.
Sub-agents cannot be used to evade a budget
A session's descendantUsage is everything its tree spent, and
limits.maxInputTokensPerSession is checked against the total on the root
session. A session.usage event carries its descendants' usage too, and the rollup
sums only own usage — counting both would bill every sub-agent turn twice.
Traces
Separately from cost, every turn writes spans.
const trace = await client.traces.forSession({ id: sessionId, includeDescendants: true })The control plane renders that as a waterfall, with infrastructure spans hidden behind a toggle by default (an unfiltered turn is about seventy rows).