What is Ringside
Ringside is one OpenAI-compatible API in front of 19 LLM providers, with a per-customer spend-metering layer your own product can bill on top of. One key replaces a drawer full of provider keys. A model ref like fc:openai/gpt-4o-mini picks the upstream, an FC-Customer header attributes the cost to one of your end users and the response tells you exactly what you were charged. That last part is the reason Ringside exists.
The problem
Putting AI in a product is easy until you have to operate it. You wire up the OpenAI SDK, ship a feature and then the questions start: which of our customers spent what last month, who is about to blow through their plan allowance, how do we cap a free-tier user at $5 of inference and how do we add Claude and Gemini without a second and third integration. None of that is in the provider SDK. It is the layer you end up writing yourself.
Calling providers directly leaves you holding several problems at once. You manage one API key per provider and rotate them by hand. You get no per-end-user cost attribution, because the provider only knows your account, not your tenant. You have no budgets, no prepaid wallets and no safe way to call from a browser, because a provider key in client-side code is a key anyone can lift. Ringside is the layer that closes those gaps while staying wire-compatible with the OpenAI API you already know.
Direct provider calls vs Ringside
| Concern | Calling providers directly | Ringside |
|---|---|---|
| Keys | One key per provider, rotated by hand | One developer key (ko_ + 64 hex), fc: model ref selects the provider |
| Switching model | Re-integrate or branch your code per provider SDK | Change the model ref string; match: / slot: / dyn: resolve at call time |
| Cost attribution | Provider sees your account only, never your end user | FC-Customer header attributes every call to a cus_ customer |
| Per-user budgets | Build and enforce it yourself | monthly_budget_usd per customer; over budget returns 402 customer_budget_exceeded |
| Prepaid balances | Not a provider concept | Per-customer wallet via /wallet/topup; empty returns 402 customer_wallet_empty |
| Browser calls | Unsafe: a provider key in the page is exfiltratable | Short-lived client token (Authorization: Client <jwt>), minted server-side |
| What you were charged | Reconcile from a monthly invoice | Every response carries X-Ringside-Wallet-Deducted and X-Ringside-Wallet-Balance |
| Resale margin | Spreadsheet math after the fact | X-FC-Billable-Amount override; read margin at GET /v1/customers/{id}/margin |
| RAG | Run your own vector DB and embedding pipeline | Managed vector stores + the file_search tool, tenant-isolated |
| Events | Poll, or build your own notifier | Signed webhooks (X-FC-Signature, HMAC-SHA256) on budget, wallet, batch and more |
The mental model
Three nouns carry most of Ringside. A developer key authenticates your backend. A model ref tells Ringside which upstream to call. A customer is one of your end users, the unit spend is attributed to and budgeted against. Everything else (wallets, client tokens, webhooks, vector stores) hangs off those three.
your app / backend Ringside (api.fightclub.pro/v1) providers
────────────────── ───────────────────────────────────── ──────────────
┌───────────────────────────────────┐
Authorization: Bearer ──▶ │ auth: ko_ dev key / Client jwt │
ko_<64 hex> │ │
FC-Customer: cus_42 ──▶ │ metering + attribution layer │ ──▶ fc:openai/...
model: fc:openai/... │ • resolve model ref │ ──▶ fc:anthropic/...
│ • enforce budget + rpm/tpm │ ──▶ fc:google/...
│ • deduct dev or customer wallet │ ──▶ ... 19 providers
◀── 200 + headers: │ • record usage + margin │
X-Ringside-Wallet- │ • cache / moderate / RAG │ ◀── raw cost
Deducted, -Balance, └───────────────────────────────────┘
X-Request-IdThe metering layer is the part you cannot get from a provider. On the way in it resolves the model ref, checks the attributed customer against its budget and rate limits, and reserves spend. On the way out it deducts from the right wallet (yours, or the customer's when one is attributed), records usage and margin, and writes the running balance and the exact charge into response headers. You read those headers and bill your own customer on top, with whatever markup you set.
A first call
If you already call OpenAI, the migration is two edits: point the base URL at https://api.fightclub.pro/v1 and prefix the model with fc:. A bare name like gpt-4o is rejected with 400 invalid_model_ref, because Ringside needs the prefix to know which of the 19 providers you mean.
curl https://api.fightclub.pro/v1/chat/completions \
-H "Authorization: Bearer $FC_API_KEY" \
-H "FC-Customer: cus_42" \
-H "Content-Type: application/json" \
-d '{"model":"fc:openai/gpt-4o-mini","messages":[{"role":"user","content":"Hello"}]}'
# Response headers tell you what happened:
# X-Request-Id: req_8f3c... (quote this in support tickets; it is a header, never a body field)
# X-Ringside-Model-Resolved: fc:openai/gpt-4o-mini
# X-Ringside-Wallet-Deducted: 0.000214 (the exact charge, in dollars)
# X-Ringside-Wallet-Balance: 48.213900
# X-RateLimit-Remaining: 59A developer key is literally ko_ followed by 64 hex characters, SHA-256 hashed at rest, created in the dashboard at /app/api-keys. Ringside has no separate test and live key classes. Use a low-budget customer or a throwaway customer id to sandbox.
Model references
Every model is named by a prefixed ref so Ringside can resolve, pin or alias it. The prefix is mandatory.
- fc:<provider>/<model>
- Pinned to one exact model, e.g.
fc:openai/gpt-4o-mini. This is the one you reach for first. - match:<provider>/<family>
- Resolves to the best current member of a family, e.g.
match:anthropic/sonnet>=4. The concrete pick comes back inX-Ringside-Model-Resolved. - slot:<alias>
- A named alias you define, so you can repoint "the cheap chat model" centrally without redeploying callers.
- dyn:<name>
- A dynamic profile that picks a model per request; the choice and reason return in
X-Ringside-Dynamic-ProfileandX-Ringside-Dynamic-Reason.
A region suffix attaches to fc: refs only, for data residency: @eu / @us keep OpenAI in-region, @eu-bedrock / @us-bedrock route Claude through AWS Bedrock, and @eu-vertex / @us-vertex route Claude through Google Vertex. An unknown suffix is a 400. You can also pass an array as the model to run a cost-aware cascade (try cheap first, escalate on a 5xx), though a cascade cannot combine with stream:true or with an idempotency-key.
Customers and spend
A customer (cus_) is a first-class object representing one of your end users or tenants. You can carry your own id on it as external_id and then address it anywhere as ext:<your-id>, which means you never have to store the Ringside id if you do not want to. Attribution is a single header.
| Customer field | Effect |
|---|---|
external_id | Your tenant/user id. Address the customer later as ext:<external_id> on every subroute. |
display_name | Human-readable label in the dashboard. |
monthly_budget_usd | Spend ceiling. Over it, calls return 402 customer_budget_exceeded (your own wallet still has money). |
rpm_limit / tpm_limit | Per-customer requests- and tokens-per-minute caps. |
default_billable_markup_pct XOR default_billable_amount_usd | What you bill this customer: a markup percent or a flat per-call amount. Setting both returns 400 billable_defaults_mutex. |
metadata | Free-form JSON, up to 4 KB serialized, 16 keys max. |
Attribute a call with the FC-Customer header. Its value can be a cus_ id, an ext:<external_id>, or a plain external id string. By default an unknown id lazily creates the customer; send FC-Customer-Strict: true to turn that off and get a 404 instead. Each call deducts from your developer wallet (empty returns 402 wallet_empty) unless a customer with its own prepaid wallet is attributed, in which case the customer wallet pays (empty returns 402 customer_wallet_empty).
Set X-FC-Billable-Amount (micro-dollars) to override the metered amount for a single call, or set a customer default markup. Margin is billable minus raw cost; read it at GET /v1/customers/{id}/margin. This is how you turn raw inference cost into a line item your own pricing sits on top of.
Calling from a browser
Never put a ko_ developer key in client-side code. Instead your backend mints a short-lived client token scoped to one customer via POST /v1/customers/{id}/client_tokens (the developer key needs the api:client_tokens scope). The browser then sends Authorization: Client <jwt>, using the Client scheme rather than Bearer. That scheme is exactly how Ringside tells a client token apart from a developer key; the header parser accepts only Bearer or Client and returns 401 otherwise.
- ·Token id (
jti) is prefixedctok_. TTL defaults to 900s, min 60, max 3600. - ·Rate limit defaults to 60 rpm, max 600, also clamped to the customer's own rpm.
- ·Optional
origin_allowlist(each entry <=512 chars) andbind_iplock the token down further. - ·Minting is rate-limited to 100 per minute per customer.
Errors, ids and rate limits
Errors share one shape, and you should always branch on the machine-readable code, never the human message. type, code and message are always present; param appears on field-validation errors and retry_after_seconds on rate-limit errors. Cross-tenant access returns 404 (not_found_error), never 403, so object ids are not enumerable.
{
"error": {
"type": "wallet_error",
"code": "customer_budget_exceeded",
"message": "Customer cus_42 has exceeded its monthly budget.",
"param": null
}
}Rate limiting runs in four layers and the lowest one wins: edge per-IP, per-client-token, per-developer and per-customer. Watch X-RateLimit-Remaining on every response; on a 429 read the integer Retry-After (seconds) and back off. There is no X-RateLimit-Limit or X-RateLimit-Reset header. Object ids are short and prefixed, so you can tell them apart at a glance: customer cus_, file file_, request req_, client token ctok_, assistant asst_, thread thread_, run run_, vector store vs_, batch batch_, conversation conv_ and webhook wh_.
What else is in the box
The metering and attribution model carries through every endpoint, not just chat. The rest of the surface exists so you can build a full product without bolting on a second backend.
- Managed RAG
- Create a vector store (
vs_), ingest files asynchronously and query through the OpenAIfile_searchtool inside an Assistants run. Stores are per-customer tenant-isolated; turn ongraphrag_enabledandfile_searchalso returns afacts[]array of subject/predicate/object relationships next to chunk citations. - Webhooks
- Register an HTTPS endpoint at
POST /v1/webhooksand Ringside delivers signed events (customer budget exceeded,wallet.low, batch completed and more). Each delivery carriesX-FC-Signature: t=<unix>,v1=<hex hmac_sha256>; verify constant-time. The signing secret (whsec_) is shown once. - Assistants / threads / runs
- An OpenAI-compatible shim for stateful agents. An assistant is reusable config, a thread holds the message list (cap 5,000) and a run executes one against the other, pausing at
requires_actionwhen it calls a function tool. - Batch
- Submit up to 50,000 requests against
/v1/chat/completions,/v1/embeddingsor/v1/moderationsfor half the synchronous price, with a24hcompletion window. - Moderation
POST /v1/moderationsmeters at $0, so you can screen inline for free, or set themoderationparam on a chat call to screen the prompt and/or completion.- Idempotency
- Send the lowercase
idempotency-keyheader on any endpoint to de-dupe retries within a 24 h window, scoped per (developer, customer, key).
Who it is for
Ringside fits any product where the cost of an LLM call belongs to one of your end users, and you need to see, cap and bill that cost.
- ·SaaS apps adding AI features that have to track and limit spend per tenant or per plan.
- ·AI gateways and resellers that mark up inference and need margin reporting out of the box.
- ·Multi-tenant agent platforms that run many isolated workloads under one account.
- ·RAG products that want managed vector stores and
file_searchwithout operating a vector DB.
If you are a single internal service calling one provider with no notion of downstream customers, you do not need most of this and can keep calling the provider directly. The moment a second tenant, a budget or a browser enters the picture, the metering layer starts paying for itself.
Versioning
The only version is the /v1 path segment. There is no version header and no Stripe-Version equivalent.
Next steps
Make your first authenticated call, create a customer and read the wallet headers end to end.
How the metering layer resolves refs, enforces budgets and attributes spend across 19 providers.
Developer keys, customers, wallets, client tokens and model refs, defined in one place.
Every parameter, header, default and error code for the core endpoint.