Skip to content
Relaykit
Tutorials

Give Every Project Its Own API Key and Spend Limit

One key per project and environment, each with a spend limit sized from real traffic, an expiry when it should end and an alert when it is blocked.

Relaykit TeamPublished 7 min read
On this page

Most teams start with one API key. It goes into the first prototype, then into the second project, then into CI, and then into a teammate’s coding agent. Six months later nobody knows which project spends what. You can’t revoke the key without breaking everything, and one runaway loop can use up the whole balance.

The fix is simple and takes about fifteen minutes: one key per project per environment, each with its own spend limit. This tutorial covers how to name the keys, how to size each limit from real numbers, when to set an expiry, and how your code should behave when a key is blocked.

What you get from separate keys

  • A smaller blast radius. A leaked or misused key can only spend up to its own limit, not your whole balance.
  • Cost per project. The usage log shows which key made each request, so “what does the invoice bot cost?” takes one filter to answer.
  • Rotation with no coordination. You can rotate the staging key without touching production or asking anyone else to update their config.
  • Clean offboarding. When a contractor leaves or a demo ends, you delete one key and nothing else breaks.

Step 1: decide the keys before you create them

Write the list first. A naming scheme that sorts well and tells you the owner at a glance is project-environment, plus a person’s name for personal tools:

Key name Used by Why it is separate
invoice-bot-prod Production server Real traffic, the largest limit, rotated on a schedule
invoice-bot-dev Local development Small limit. Stray test loops can’t touch production.
invoice-bot-ci CI test runs Lives in CI secrets only, and is easy to rotate after a leak in logs
support-search-prod Second product Its own budget and its own usage line
alice-claude-code One developer’s coding agent Agent usage is personal and spiky, so each developer gets a key
acme-demo-oct A one-off demo Has an expiry date and is deleted afterwards

Avoid names like key1 or new-key-final. You’ll read these names in the usage log during an incident, so they should explain themselves.

Step 2: size each spend limit from real numbers

A spend limit is the most this key can ever spend. Set it from expected traffic plus headroom, not from a round number you picked. Here is a worked example for a support bot on Claude Haiku 4.5, using our current rates of $1.10 input and $5.50 output per 1M tokens:

Assumption Value
Requests per day 3,000
Input tokens per request (prompt + context) 1,500
Output tokens per request 300
Input per month 135M tokens → $148.50
Output per month 27M tokens → $148.50
Expected month $297.00
Limit with 1.5× headroom, rounded up $450.00

Get the per-request numbers from real responses, not guesses. Every response includes a usage object with the exact input and output tokens. Log it for a day of traffic and take the average. If you don’t have traffic yet, the cost calculator and our guide to estimating monthly LLM cost will get you close.

For the other keys:

  • Development and CI keys: small limits, enough for a week or two of normal work. When a test loop runs away, it should hit the limit within minutes, not after a day of spending.
  • Coding agents: agent spend is uneven. A long refactor on Claude Sonnet 5 can use more in an afternoon than a quiet week. Start with a modest limit, look at the usage log after a week, and adjust.

The limit is a total, not a monthly allowance. In Relaykit, a key’s spend limit counts down as the key is used and does not reset by itself. Many teams handle this with a monthly routine: look at last month’s usage per key, then raise each limit by next month’s budget. This keeps a person checking spend at least once a month, which is the main point of having limits.

Your account balance is the ceiling above all keys. A request needs room in both the key’s limit and the account’s prepaid balance. The limits on all your keys can add up to more than your balance. That’s fine, because no key can spend money the account doesn’t have.

Step 3: set an expiry when the key has an end date

Some keys should stop working on a date: a contractor’s engagement, a hackathon, a demo for one customer, a load test. Set an expiry when you create them. After that date the key returns 401 and needs no cleanup to be safe. Don’t set expiries on production keys unless you have a reminder to rotate first, or your app will stop on a weekend.

Step 4: create the keys in the console

In the API keys page of the console, create each key from your list with its name, spend limit and (if needed) an expiry date. If a key only needs one model, restrict it to that model. Then a leaked production key can’t be used to run an expensive flagship model.

Copy each new key straight into the place it will live, and nowhere else (not a chat message, not a shared doc):

  • Servers: your host’s secret store or environment settings.
  • CI: the CI provider’s encrypted secrets, scoped to the one repository.
  • Local development: a .env file that is listed in .gitignore.

Give each project its own variable name. Then a key in the wrong project fails loudly, instead of quietly charging the wrong budget:

.env.example / .env
# .env.example  (committed: names only, no values)
INVOICE_BOT_LLM_KEY=
SUPPORT_SEARCH_LLM_KEY=

# .env  (never committed: real values, one key per project)
INVOICE_BOT_LLM_KEY=sk-...
SUPPORT_SEARCH_LLM_KEY=sk-...

Step 5: handle a blocked key on purpose

When a key reaches its spend limit, or the account balance runs out, the API answers with 403 before calling the model, so that request costs nothing. A revoked or expired key gets 401. Retrying won’t help with either, so your code should notice, tell a person, and degrade gracefully.

Python: invoice bot
import logging
import os

from openai import OpenAI, APIStatusError

log = logging.getLogger("invoice-bot")
client = OpenAI(base_url="https://api.oneaiapikey.com/v1", api_key=os.environ["INVOICE_BOT_LLM_KEY"])

def summarize(invoice_text: str) -> str | None:
    try:
        resp = client.chat.completions.create(
            model="claude-haiku-4-5",
            max_tokens=400,
            messages=[{"role": "user", "content": "Summarize this invoice:\n" + invoice_text}],
        )
        return resp.choices[0].message.content
    except APIStatusError as e:
        if e.status_code == 403:
            # Spend limit or balance used up, or the model is not allowed for this key.
            log.error("LLM key blocked (403): %s", e.message)
            notify_owner("invoice-bot: LLM key blocked, check the console")
            return None  # degrade: the invoice is saved without a summary
        if e.status_code == 401:
            log.error("LLM key rejected (401): revoked or expired")
            notify_owner("invoice-bot: LLM key revoked or expired")
            return None
        raise  # 429 and 5xx: let your retry logic handle them
Node: support search
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.oneaiapikey.com/v1",
  apiKey: process.env.SUPPORT_SEARCH_LLM_KEY,
});

export async function answer(question) {
  try {
    const resp = await client.chat.completions.create({
      model: "claude-haiku-4-5",
      max_tokens: 500,
      messages: [{ role: "user", content: question }],
    });
    return resp.choices[0].message.content;
  } catch (err) {
    if (err instanceof OpenAI.APIError && (err.status === 401 || err.status === 403)) {
      console.error("LLM key blocked:", err.status, err.message);
      await notifyOwner("support-search: LLM key blocked (" + err.status + ")");
      return null; // show "search is unavailable" instead of a stack trace
    }
    throw err;
  }
}

notify_owner and notifyOwner stand for whatever reaches a person: an email, a chat message, an issue in your tracker. Make sure it can’t fire a thousand times a minute. Send one alert per key per hour. The errors reference lists every status code and whether to retry it.

Step 6: rotate without downtime

Rotate production keys on a schedule, and right away if one might have leaked:

  1. Create a new key with the same name plus a date, for example invoice-bot-prod-2026-11, and the same limit.
  2. Deploy it to the app’s secret store and restart or redeploy.
  3. Watch the usage log until the old key has made no requests for a day.
  4. Delete the old key.

If a key has leaked, swap steps: delete it first and accept a few minutes of errors. A leaked key that is spending money is worse than a short outage.

A weekly five-minute check

  • Open the usage log and group by key. Does any key’s spend look unexpected?
  • Is any key close to its limit? Raise it on purpose, or find out why.
  • Is there a key nobody recognises? Delete it.
  • Is any key past its project’s end date? Delete it.

Checklist

  • One key per project per environment, plus one per developer for coding agents.
  • Names that explain themselves in the usage log.
  • Each spend limit sized from measured tokens per request, with headroom.
  • Expiry dates on keys that should end.
  • Keys stored only in secret stores and git-ignored .env files.
  • Code that treats 401 and 403 as “alert a person and degrade”, not “retry”.

For more on keeping keys out of browsers, logs and repositories, see API key security for AI apps.

More in Tutorials

Try it

One API key for Claude, GPT, Grok and DeepSeek.

Relaykit speaks the OpenAI and Anthropic formats, so you change the base URL and the key, not your code. Top-ups start at $3, credit packs at $10, and every model has a published per-token rate.