Short answer: one account-wide spending cap isn't enough. Set limits at three levels: a monthly limit for the whole team, a monthly budget per developer (covering all their keys), and a monthly budget per key, so each project and environment has its own ceiling. Add model allowlists so cheap environments can't call expensive models, give temporary keys an expiry date, and set alerts that fire well before any limit is hit. On the LazySusan API all three limits apply to every request, and a request that would go over is refused before it runs, so a runaway loop stops at the cap instead of at the invoice.
Below: how the limits work, how to size them, and how to handle a budget error in code without breaking your product.
Why a single cap fails
Most AI API budgets start as one number: "don't spend more than $2,000 a month." That number protects you from the worst case, but it protects you badly:
- The wrong thing hits it. A load test in staging can burn the month's budget, and production stops with it.
- Nobody owns it. When the cap is shared, nobody feels responsible until it's reached.
- It tells you nothing. Hitting a global cap says you spent too much, not where.
Budgets work when they map to how you actually build: per person, per project, per environment.
The three limits
LazySusan has three spend limits, and all of them apply to every request:
| Limit | Set on | What it covers | Error when reached |
|---|---|---|---|
| Monthly limit | The whole team (Settings) | Everything the team spends | 402 monthly_limit_reached |
| Member budget | One person (Team) | All of that person's keys together | 402 member_budget_reached |
| Key budget | One key (Keys) | Just that key | 402 key_budget_reached |
A few rules that matter in practice:
- Limits are checked before the request runs. If it could push any of them over, it's refused with a 402 and nothing is charged.
- In-flight requests count. Credits held by requests that are still running count toward each limit, so fifty parallel calls can't sneak past a budget together.
- Budgets reset monthly at the start of each calendar month, UTC.
- The team balance is a limit too. If the prepaid balance can't cover a request, you get
402 insufficient_balance. You're never billed past zero.
Projects and environments are keys
There's no separate "project budget" setting, and you don't need one: every key carries a project and an environment, so a budget on that key is a budget for that project in that environment. If a project has several services, give each its own key and its own slice of the budget.
A sizing plan for a six-person team building two AI features might look like this (1 credit = $0.001):
| Key | Project | Environment | Monthly budget | Allowed models |
|---|---|---|---|---|
| support-bot-prod | support-bot | production | 400,000 ($400) | Claude Sonnet 5.5, Gemini 3.8 Flash |
| support-bot-staging | support-bot | staging | 30,000 ($30) | Gemini 3.8 Flash, Claude Haiku 5.5 |
| search-prod | search | production | 250,000 ($250) | Sonar, Sonar Pro |
| ci-evals | evals | test | 50,000 ($50) | any |
| each developer's laptop key | (none) | development | 20,000 ($20) | any except GPT-6 Astra and Claude Fable 5.1 |
Then on top:
- Member budgets of 40,000 credits for most developers, so a person with three keys still has one ceiling.
- A team monthly limit a little above the sum of production budgets, as the backstop.
Start generous on production and tight everywhere else. Production running out is an incident; a laptop running out is a nudge.
How to size a budget you've never measured
If a feature is new, you don't have history. Estimate from the shape of a request:
- Measure one request. Every response includes
usage.costCredits. Run twenty realistic examples and take the average. - Multiply by expected volume. Requests per day × 30.
- Add 50% headroom for launch spikes, retries and the prompt growing over time.
- Revisit after two weeks with real numbers from the dashboard.
For a sense of scale, a typical support-style request with about 1,500 tokens in and 400 out costs roughly 8 credits on Claude Sonnet 5.5, about 3 on Gemini 3.8 Flash and well under 1 on Claude Haiku 5.5, using the current rate card. At 2,000 requests a day, that's the difference between around $480 and around $24 a month, which is usually the first budget conversation worth having.
Model allowlists: the budget's quieter partner
Budgets cap how much. Allowlists cap what. Give a key or a member a list of allowed models, and anything else is refused with 403 model_not_allowed, free of charge.
Good uses:
- Staging and CI on cheap models only. Your tests check that the plumbing works; they don't need a flagship model.
- Keep the most expensive models for the work that needs them. GPT-6 Astra and Claude Fable 5.1 cost several times more than the everyday models. Allow them on the keys where someone has decided they're worth it.
- Pin production to what you tested. If your evals ran on Claude Sonnet 5.5, a typo in an environment variable shouldn't quietly switch production to something else.
Member lists and key lists work together: a member's list applies to every key they own, and a key's own list can narrow it further but never widen it.
Expiry for anything temporary
Contractor access, a hackathon, a load test, a demo for a client: give the key an expiry date when you create it. After that date it fails with 401 api_key_expired. It's the cheapest way to make sure temporary access stays temporary.
Handle budget errors in code
A budget error is a decision your future self made on purpose, so treat it as a normal outcome rather than a crash. The SDK raises LazySusanError with a code, and there are helpers for the common cases:
import LazySusan, { LazySusanError } from "lazysusan";
const ls = new LazySusan();
export async function draftReply(ticket) {
try {
const r = await ls.chat.create({
model: "anthropic/claude-sonnet-5-5",
messages: [{ role: "user", content: ticket.text }],
maxTokens: 800,
metadata: { feature: "draft-reply", customer: ticket.accountId },
});
return { ok: true, text: r.text };
} catch (err) {
if (LazySusanError.isBudgetReached(err)) {
// key, member or team budget reached: nothing was charged
await notifyOncall(`AI budget reached: ${err.code}`);
return { ok: false, reason: "budget" };
}
if (LazySusanError.isInsufficientBalance(err)) {
return { ok: false, reason: "balance" };
}
throw err;
}
}In Python, the same checks are properties on the error:
from lazysusan import LazySusan, LazySusanError
ls = LazySusan()
try:
r = ls.chat.create(model="anthropic/claude-sonnet-5-5", messages=messages, max_tokens=800)
except LazySusanError as err:
if err.is_budget_reached:
notify_oncall(f"AI budget reached: {err.code}")
else:
raiseWhat you show the user is a product decision: hide the AI button, queue the job for next month, or show the non-AI path. What you shouldn't do is retry on another model with the same key. The key budget applies to every model.
Mind max_tokens: it affects what a request can start
Before a request runs, LazySusan holds the most it could cost, which for chat is the full prompt plus maxTokens of output. You're charged only for what's used and the rest is released immediately, but the hold has to fit inside the balance and every budget.
So a key close to its budget, calling an expensive model with maxTokens: 32000, can be refused even though the answer would have been short. Set maxTokens to what the feature actually needs. It keeps holds small, and it stops a confused model from writing a novel.
The same logic applies to video: a clip's price is held while it renders, then charged on success or returned in full on failure.
Alerts: know before the wall
Limits stop spend. Alerts tell you it's coming. Owners and admins can set email alerts, each with up to 10 addresses:
| Alert | Use it for |
|---|---|
key_budget_used | Any key past, say, 80% of its monthly budget |
monthly_spend_above | Team spend this month past a threshold |
daily_spend_above | Catching a runaway job on the day it starts |
balance_below | The prepaid balance getting low |
error_rate_above | More than a set share of last hour's requests failing (at least 20 requests) |
Alerts are checked every 5 minutes and email once when the condition starts, then wait until it clears, so a bad day produces one email, not two hundred.
Pair balance_below with auto top-up: pick a threshold and an amount, and when the balance drops below it, your saved card is charged and credits are added straight away. That keeps the balance from being the limit nobody meant to set.
Tags report; keys enforce
Request metadata (up to 16 tags such as feature, customer or experiment) lets you filter the dashboard and request log by anything you like. It's the right tool for "what does the summaries feature cost per customer?"
But tags don't enforce limits. If a feature needs a hard ceiling, give it its own key. Use tags to understand spend and keys to stop it. There's more on reading those numbers in AI API cost tracking for engineering managers, and on splitting keys in how to give every developer their own API key.
FAQ
Can I set a spending limit per API key? Yes. On the LazySusan API every key can have a monthly budget in credits. Once a request would take it past the budget, the key gets 402 key_budget_reached until the next calendar month, and nothing is charged.
Can I set a budget per developer? Yes. A member budget caps one person's spend across all of their keys, so creating more keys doesn't get around it.
How do I limit which AI models a key can use? Give the key (or the member) a list of allowed models. Requests for any other model are refused with 403 model_not_allowed and aren't charged.
When do API budgets reset? At the start of each calendar month, UTC.
What happens when the prepaid balance runs out? Requests the balance can't cover are refused with a 402 before they run, so you're never billed past zero. Auto top-up refills the balance from a saved card when it drops below a threshold you choose.