Usage and spend caps
Read model usage, track estimated cost, and set environment and API key budgets.
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.descendantUsagegroupBy is day, agent, key or identity. The last breaks the period down by
the identity each session asserted, as scope:id:
const byPerson = await client.usage.environment({ period, groupBy: "identity" })
// [{ key: "user:ada", totals: { … } },
// { key: "anonymous", label: "No identity asserted", totals: { … } }]Sessions without an asserted identity appear under anonymous. Older usage without
that dimension appears under unknown. Use scope:id to join an identity breakdown
to your application's users. Breakdowns are alternate views of the same totals.
costUsd is a lower bound whenever unpricedCalls > 0. A model Cereal's price
table does not know contributes nothing rather than a guess. Cost is computed per
model call rather than from a turn's summed tokens, because a turn that fell down a
fallback ladder used two models at two prices.
Caps
await client.environments.update({
update: { slug: "dev", spend: { costUsdPerMonth: 50, tokensPerMonth: 20_000_000 } }
})
await client.apiKeys.update({ update: { id: keyId, spend: { costUsdPerMonth: 10 } } })A key's cap may not exceed its environment's (SpendCapExceedsParentError).
Caps are checked in exactly three places and nowhere else: sessions.create,
sessions.send, and when an agent spawns a sub-agent.
A spend cap does not interrupt active turns or block approval decisions, so usage can exceed the cap during work already in progress.
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}`)
}
}SpendLimitExceeded returns HTTP 402. Raise the cap or wait for the reported reset
time before starting more work.
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, because counting both would bill every sub-agent turn twice.
Traces
Separately from cost, every turn writes spans: the turn, each model request, each tool
call, each compaction. Read them with traces.forSession; see
traces.