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#

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.