# 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.

Canonical: https://getmux.io/guides/per-agent-spend-limits


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](https://getmux.io/docs/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.

```bash
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"}'
```
```ts
import { Mux } from "getmux";

const admin = new Mux({ apiKey: process.env.MUX_MGMT_KEY });

const { key, data } = await admin.keys.create({
  name: "research-agent",
  limit: 10,
  limitReset: "daily",
});
// key  -> "mux-..." store it now; it is never returned again
// data -> key object with id, limit, limitRemaining, limitReset
```
```python
from getmux import Mux

admin = Mux(api_key=os.environ["MUX_MGMT_KEY"])

secret, key = admin.keys.create("research-agent", limit=10, limit_reset="daily")
# secret -> "mux-..." store it now; it is never returned again
# key    -> ApiKey with id, limit, limit_remaining, limit_reset
```

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.

```json
{
  "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:

```ts
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;
}
```
```python
from getmux import Mux, MuxError

mux = Mux(api_key=tenant_key)

try:
    res = mux.search(query, models=["parallel/fast", "exa/auto"])
    results = res.results
except MuxError as e:
    if e.code == "key_limit_exceeded":
        # Tell the agent its search budget is spent; let it finish with what it has.
        results = []
    else:
        raise
```

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).

```bash
curl https://api.getmux.io/v1/key -H "Authorization: Bearer $MUX_API_KEY"
```
```ts
const me = await mux.key();
if (me.limitRemaining !== null && me.limitRemaining < 0.05) {
  console.warn("search budget nearly spent", me.limitRemaining);
}
```
```python
me = mux.key()
if me.limit_remaining is not None and me.limit_remaining < 0.05:
    print("search budget nearly spent", me.limit_remaining)
```

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.

```ts
const usage = await admin.usage(30);
for (const row of usage.byKey) console.log(row.name, row.requests, row.costUsd);
```
```python
usage = admin.usage(30)
for row in usage["by_key"]:
    print(row["name"], row["requests"], row["cost_usd"])
```

The [dashboard](https://getmux.io/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.

```bash
# 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"
```
```ts
await admin.keys.update(keyId, { disabled: true });   // kill switch
await admin.keys.update(keyId, { disabled: false });  // back on
await admin.keys.delete(keyId);                       // permanent
```
```python
admin.keys.update(key_id, disabled=True)    # kill switch
admin.keys.update(key_id, disabled=False)   # back on
admin.keys.delete(key_id)                   # permanent
```

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.

```ts
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 });
}
```
```python
from getmux import Mux

admin = Mux(api_key=os.environ["MUX_MGMT_KEY"])

def provision_tenant(tenant_id: str, plan: str):
    secret, key = admin.keys.create(
        tenant_id,
        limit=50 if plan == "pro" else 2,
        limit_reset="monthly",
    )
    db.tenants.update(tenant_id, mux_key_id=key.id, mux_key=encrypt(secret))

def upgrade_tenant(tenant_id: str):
    admin.keys.update(db.tenants.get(tenant_id).mux_key_id, limit=50)

def suspend_tenant(tenant_id: str):
    admin.keys.update(db.tenants.get(tenant_id).mux_key_id, 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](https://getmux.io/docs/quickstart).
