Account: $595 bought / $567 used | Keys capped: 3 of 4 | Date: 2026-09/10 | Status: diagnosed Three of my four OpenRouter keys were returning Key limit exceeded. The account balance was $28.
"Key limit exceeded" reads like "out of credits". It is not. It is a per-key spend cap, and it fires independently of the account balance.
What the state looked like
Each project has its own key with its own limit. That is deliberate: one runaway job can only burn its own budget.
| Key | Limit | Used | State |
| content project A | $30 | $30 | capped |
| backend project B | $40 | $40 | capped |
| SEO project C | $10 | $10 | capped |
| content project D | $10 | $5 | healthy |
| Account | $595 bought | $567 used | $28 left |
Three keys at exactly 100% of their limit. The account itself was fine. Topping up the account balance would have fixed nothing, because the cap sits on the key.
Why it misleads
The failure looks the same to a retry wrapper whether the account is empty or one key is capped. The account balance, the number everyone watches, said $28. Three pipelines were failing anyway.
Two failure classes, same symptom:
- Account empty: fix is a payment. Every key dies at once.
- Key capped: fix is raising that one key's limit. Other keys keep working.
The second one is partial: other keys keep working, so nothing looks like a full outage.
The two calls that separate them
# Per-key: limit, usage, remaining for the key you pass
curl -s https://openrouter.ai/api/v1/key \
-H "Authorization: Bearer $OPENROUTER_API_KEY" | jq '.data | {limit, usage, limit_remaining}'
# Account-wide: total bought vs total used
curl -s https://openrouter.ai/api/v1/credits \
-H "Authorization: Bearer $OPENROUTER_MGMT_KEY" | jq '.data | .total_credits - .total_usage' Run the first one once per project key. limit_remaining: 0 with a healthy second call is the whole diagnosis. Print both in one table and a capped key shows up as a row, not as a mystery.
The same class of bug, one layer over
The scheduler that runs my agent sessions fails over between two cheap providers. Both ran out of quota within the same stretch. The failover logic only recognised certain exhaustion messages. One provider answered with a 403 saying a weekly usage limit was reached. The other included a reset timestamp in a bracketed error code. Neither matched the patterns, so the code treated them as ordinary failures instead of quota exhaustion.
Result: 114 of 114 scheduled sessions failed, each one retried against a provider that was never going to answer until the reset.
Quota errors are a distinct class. They need their own branch: do not retry, mark the provider exhausted until the reset time, move on. Matching on message text is fragile, because every provider words it differently and changes it without notice.
Budget numbers worth knowing
Burn rate on the account was about $8 per week. Balance by date:
| Date | Remaining |
| Oct 4 | ~$17 |
| Oct 11 | $9.17 |
| Oct 18 | $17.93 (after a $10 top-up) |
At $8/week, $9 is a bit over a week of runway. Per-key caps hit first because they were sized per project, not per account.
What I'd change
Per-key caps are the right design, and I would keep them. I would stop treating them as invisible. Each key gets a daily limit_remaining reading logged next to the account balance, with an alert at 20% remaining per key. And quota detection moves from string matching to status plus a provider-specific parser, with a default of "unknown error from a provider that previously worked means assume exhausted, back off".