If you resell or expose LLM access, you need each customer to have their own metered wallet — not a shared key that any one user can drain. By the end of this walkthrough you'll have a self-hosted LiteLLM proxy that mints a distinct API key per user, each with a hard dollar cap, per-key rate limits, and automatic USD cost tracking you can read back over HTTP. It runs on one process plus a Postgres database, and every key speaks the OpenAI Chat Completions format, so your users' existing clients work unchanged.
1. Stand up the proxy#
Install the proxy and give it a config file. Two things make budgets work: a master key (your admin credential, must start with sk-) and a Postgres database_url where keys and spend counters live. Without the database, there are no virtual keys.
pip install 'litellm[proxy]'
export DATABASE_URL="postgresql://user:pass@localhost:5432/litellm"
# config.yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
general_settings:
master_key: sk-1234 # your admin key
database_url: "os.environ/DATABASE_URL" # where keys + spend live
Start it:
litellm --config /path/to/config.yaml
# proxy up on http://0.0.0.0:4000
The model_name values are the public model IDs your users request. What they map to upstream is your business — see building a cost-aware model router for how to keep the cheap path cheap behind these names.
2. Mint a virtual key with a budget#
You call /key/generate with the master key in the Authorization header. The important fields: max_budget (a USD cap) and budget_duration (the reset window). Tag the key with user_id so spend also aggregates per customer, and key_alias so it's readable in your dashboard.
curl 'http://0.0.0.0:4000/key/generate' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data-raw '{
"models": ["gpt-4o"],
"user_id": "customer_8842",
"key_alias": "acme-inc-prod",
"max_budget": 10,
"budget_duration": "30d"
}'
The response hands back the key your customer uses:
{ "key": "sk-RV-l2BJEZ_LYNChSx2EueQ", "user_id": "customer_8842", "max_budget": 10, "budget_duration": "30d" }
budget_duration accepts seconds ("30s"), minutes ("30m"), hours ("30h"), or days ("30d"). Omit it and the cap becomes a lifetime limit that never resets — usually not what you want for a subscription. When a key crosses its cap, the very next call is rejected before it reaches the model:
{ "detail": "Authentication Error, ExceededTokenBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07" }
Your customer then calls the proxy exactly like the OpenAI API, using their key — never the master key:
from openai import OpenAI
client = OpenAI(base_url="http://0.0.0.0:4000", api_key="sk-RV-l2BJEZ_LYNChSx2EueQ")
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "hello"}],
)
3. Enforce rate limits#
A dollar cap stops the month from blowing up; rate limits stop a single bad minute. Add rpm_limit (requests per minute) and tpm_limit (tokens per minute) at mint time — they're enforced per key.
curl 'http://0.0.0.0:4000/key/generate' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data-raw '{
"models": ["gpt-4o"],
"user_id": "customer_8842",
"max_budget": 10,
"budget_duration": "30d",
"rpm_limit": 100,
"tpm_limit": 50000
}'
Want a floor and ceiling on what any generated key can request? Set upperbound_key_generate_params (a hard maximum the proxy clamps to) and default_key_generate_params (defaults applied when a field is omitted) under litellm_settings:
litellm_settings:
upperbound_key_generate_params:
max_budget: 100
budget_duration: "30d"
rpm_limit: 1000
tpm_limit: 100000
default_key_generate_params:
max_budget: 5
Now a /key/generate request asking for max_budget: 200 is created at 100 — the upper bound wins.
4. Track spend per key#
Spend is recorded automatically, in USD, after every /chat/completions, /completions, and /embeddings call — computed by LiteLLM's completion_cost() against its model price map. You don't instrument anything. Read a key's live spend with /key/info:
curl 'http://0.0.0.0:4000/key/info?key=sk-RV-l2BJEZ_LYNChSx2EueQ' \
-X GET \
--header 'Authorization: Bearer sk-1234'
{
"key": "sk-RV-l2BJEZ_LYNChSx2EueQ",
"info": { "spend": 0.0001065, "max_budget": 10, "models": ["gpt-4o"] }
}
Because you set user_id, spend also rolls up per customer via /user/info?user_id=customer_8842 — the number you feed into invoicing. Pipe that into Stripe usage-based billing with meters and the proxy becomes the metering backend for a resale product.
5. Adjust a cap without re-issuing a key#
When a customer hits their wall mid-cycle, don't mint a new key. Grant a temporary bump with /key/update — it expires on its own:
curl -X POST 'http://0.0.0.0:4000/key/update' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
-d '{"key": "sk-RV-l2BJEZ_LYNChSx2EueQ", "temp_budget_increase": 100, "temp_budget_expiry": "10d"}'
What this doesn't cover / gotchas#
- No database, no budgets. Virtual keys and spend counters require Postgres; the in-memory quickstart can't enforce caps.
- Spend is post-hoc. The counter updates after a call completes, so a single very large request can push a key slightly past its cap before the next call is blocked. Rate limits are your guard against that.
budget_durationunits ares/m/h/d. The docs use seconds, minutes, hours, and days — don't assume calendar-month semantics.- Protect the master key. It can mint and read every key. Never ship it to a client; it belongs on the proxy host only.
- Fallbacks are separate. Budget enforcement isn't model routing — for graceful degradation see the cheap-model fallback gateway.
That's the whole loop: one proxy, one database, a /key/generate call per customer, and cost that reads back per key. It's the cheapest metering layer you can put between your users and a model bill.



