Spend limits per API key for agents and tenants
How to set spend limits per API key in Mux, so every agent, customer or environment has its own USD budget that resets on a schedule and cannot be overshot.
Mux lets you set spend limits per API key: a USD limit and an optional limit_reset of daily, weekly or monthly. Issue one key per agent, customer or environment with a management key, and each one gets its own hard budget. The limit is checked in the same atomic step as the debit, so a runaway loop or a burst of concurrent requests cannot push a key past its cap, and a blocked request costs nothing.
Why one key per agent#
A single shared search key gives you one number: total spend. It does not tell you which agent went into a loop, which customer is heavy, or whether staging is quietly eating production's budget. Worse, when something misbehaves, your only kill switch takes everything down with it.
One key per unit of accountability fixes all three problems:
- Per agent: a research agent that retries searches in a loop stops at its own cap instead of draining the account.
- Per customer: a tenant's usage is visible on its own line, and you can cut off one tenant without touching the others.
- Per environment: development and staging keys get small daily limits, production gets a monthly one.
Keys are created and administered with a management key (mux-mgmt-...), which cannot run searches itself. API keys (mux-...) can only search and read their own usage. Details are on Managing keys.
Creating a key with a limit#
POST /v1/keys takes an optional name, a limit in USD from 0 to 1,000,000, a limit_reset of daily, weekly or monthly, and an optional expires_at. The secret is returned once, on creation, and stored hashed.
curl https://api.getmux.io/v1/keys \
-H "Authorization: Bearer $MUX_MGMT_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "research-agent", "limit": 10, "limit_reset": "daily"}'Reset windows are in UTC: daily at 00:00, weekly at 00:00 on Monday, monthly at 00:00 on the 1st. With no limit_reset, the limit is a lifetime cap. A limit of 0 creates a key that can't spend, which is useful as a placeholder you raise later.
How enforcement works#
The limit check and the balance check run together, in the same step that reserves the request's price. That matters for agents because they fail in bursts: fifty concurrent tool calls arriving at once are each checked against what is left, and none of them can slip past the cap between a read and a write.
| Event | Effect on the key's limit |
|---|---|
| Successful search | Debited at the model's list price. |
| Provider call fails and is refunded | The amount is returned to the limit as well as to your balance. |
| Request would exceed the limit | Rejected with 402 key_limit_exceeded. Nothing is charged. |
| Fallback list with a too-expensive model | That model is skipped; a cheaper one later in models can still run. |
| BYOK fees | Count against the key's limit like any other charge, and are refunded if the call fails. |
Lowering a limit below what the key has already spent does not refund anything; the key is blocked until its window resets.
Handling 402 key_limit_exceeded#
When a key is out of budget, the search returns 402 with this body. The needed_usd field is the price of the cheapest model that was blocked.
{
"error": {
"code": "key_limit_exceeded",
"message": "This key's credit limit would be exceeded.",
"needed_usd": 0.001
}
}Do not retry it. Nothing changes until the window resets, the limit is raised, or a different key is used. The SDKs throw MuxError and never retry 4xx. The right handling depends on who the key belongs to:
import { Mux, MuxError } from "getmux";
const mux = new Mux({ apiKey: tenantKey });
try {
const res = await mux.search({ query, models: ["parallel/fast", "exa/auto"] });
return res.results;
} catch (e) {
if (e instanceof MuxError && e.code === "key_limit_exceeded") {
// Tell the agent its search budget is spent; let it finish with what it has.
return [];
}
throw e;
}For a customer-facing product, surface the state to the tenant ("search budget used for this month") and offer an upgrade path that calls keys.update with a higher limit.
Reading limit_remaining and usage by key#
An agent can check its own budget before starting expensive work with GET /v1/key, which works with the API key itself. The response is the key object, including limit, limit_remaining, usage (lifetime) and usage_period (current window).
curl https://api.getmux.io/v1/key -H "Authorization: Bearer $MUX_API_KEY"From the management side, GET /v1/keys/{id} returns the same object for any key, and GET /v1/usage aggregates spend by day, by model and by key over a window of up to 90 days. The by_key list is sorted by cost, highest first, so the top entry is your noisiest agent or tenant. Deleted keys still appear with their past usage.
const usage = await admin.usage(30);
for (const row of usage.byKey) console.log(row.name, row.requests, row.costUsd);The dashboard shows the same breakdown by key and by model, plus a per-request activity log with the key name on each row.
Disabling and rotating a key#
Disabling is a reversible kill switch. Deleting is permanent. Both take effect immediately on the server that handled the change and within about 15 seconds everywhere.
# kill switch
curl -X PATCH https://api.getmux.io/v1/keys/key_Hx2mQp9aLwT4kZ1v \
-H "Authorization: Bearer $MUX_MGMT_KEY" \
-H "Content-Type: application/json" \
-d '{"disabled": true}'
# permanent
curl -X DELETE https://api.getmux.io/v1/keys/key_Hx2mQp9aLwT4kZ1v \
-H "Authorization: Bearer $MUX_MGMT_KEY"A disabled key returns 401 key_disabled; an expired one returns 401 key_expired. To rotate, create the replacement first with the same name and limit, deploy it, then delete the old key. Because the secret is only shown once, rotation is the only way to recover from a lost or leaked key.
Pattern: multi-tenant SaaS with a key per tenant#
Give each tenant a key whose name is the tenant id. The name then shows up in usage reports and the activity log, so you never need a lookup table from key id to customer. Keep the mapping from tenant id to key id in your own database so you can raise, disable or delete the right key.
import { Mux } from "getmux";
const admin = new Mux({ apiKey: process.env.MUX_MGMT_KEY });
export async function provisionTenant(tenantId: string, plan: "free" | "pro") {
const { key, data } = await admin.keys.create({
name: tenantId,
limit: plan === "pro" ? 50 : 2,
limitReset: "monthly",
});
await db.tenants.update(tenantId, { muxKeyId: data.id, muxKey: encrypt(key) });
}
export async function upgradeTenant(tenantId: string) {
const { muxKeyId } = await db.tenants.get(tenantId);
await admin.keys.update(muxKeyId, { limit: 50 });
}
export async function suspendTenant(tenantId: string) {
const { muxKeyId } = await db.tenants.get(tenantId);
await admin.keys.update(muxKeyId, { disabled: true });
}At request time, build the client with the tenant's key, not your management key. The tenant's key can see only its own usage and activity, so even a leaked tenant key cannot read what other tenants are doing or open a credit purchase.
FAQ#
Can a key overshoot its limit under concurrent load?#
No. The limit check happens in the same atomic step as the debit, so parallel requests are each checked against the amount that is actually left. A request that would exceed the limit is rejected before any provider is called.
Does a blocked request cost anything?#
No. A 402 key_limit_exceeded response is free, and so are requests blocked by insufficient credits or rate limits. Failed provider calls are refunded to both your balance and the key's limit.
What is the difference between a key limit and my credit balance?#
The balance is one pool shared by all keys on the account. A key limit is a cap on how much of that pool one key may use in its window. A key can be blocked by its own limit while the account still has plenty of credit, and vice versa.
Can I change a limit without reissuing the key?#
Yes. PATCH /v1/keys/{id} changes limit, limit_reset, name and disabled in place. The secret stays the same. Only expires_at is fixed at creation.
Create your first key and run a search in a minute with the quickstart.
Also available as markdown for your coding assistant.
More guides
Add a web search tool to the Vercel AI SDK with Mux
Build a web search tool for the Vercel AI SDK with the tool() helper and multi-step generateText, using Mux for Exa and Parallel results on one key.
Exa pricing explained: what each Exa search tier costs
Exa pricing through Mux: per-request and extra-result math for exa/instant, fast, auto and deep, worked examples, and when your own Exa contract is cheaper.
Keenable search API through Mux: models and pricing
What the Keenable search API offers through Mux: two models, a flat $4 per 1,000 requests for up to 50 results, native date filters, and no page text.