Prefer Swagger UI? Click hereThe same API in the classic OpenAPI explorer.
// get started · 6 min read

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

ConcernCalling providers directlyRingside
KeysOne key per provider, rotated by handOne developer key (ko_ + 64 hex), fc: model ref selects the provider
Switching modelRe-integrate or branch your code per provider SDKChange the model ref string; match: / slot: / dyn: resolve at call time
Cost attributionProvider sees your account only, never your end userFC-Customer header attributes every call to a cus_ customer
Per-user budgetsBuild and enforce it yourselfmonthly_budget_usd per customer; over budget returns 402 customer_budget_exceeded
Prepaid balancesNot a provider conceptPer-customer wallet via /wallet/topup; empty returns 402 customer_wallet_empty
Browser callsUnsafe: a provider key in the page is exfiltratableShort-lived client token (Authorization: Client <jwt>), minted server-side
What you were chargedReconcile from a monthly invoiceEvery response carries X-Ringside-Wallet-Deducted and X-Ringside-Wallet-Balance
Resale marginSpreadsheet math after the factX-FC-Billable-Amount override; read margin at GET /v1/customers/{id}/margin
RAGRun your own vector DB and embedding pipelineManaged vector stores + the file_search tool, tenant-isolated
EventsPoll, or build your own notifierSigned 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-Id
Your app talks to one endpoint. The metering layer in the middle attributes the call, enforces the budget and reports the exact charge before forwarding to the provider.

The 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: 59
There is no test-vs-live key split

A 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 in X-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-Profile and X-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 fieldEffect
external_idYour tenant/user id. Address the customer later as ext:<external_id> on every subroute.
display_nameHuman-readable label in the dashboard.
monthly_budget_usdSpend ceiling. Over it, calls return 402 customer_budget_exceeded (your own wallet still has money).
rpm_limit / tpm_limitPer-customer requests- and tokens-per-minute caps.
default_billable_markup_pct XOR default_billable_amount_usdWhat you bill this customer: a markup percent or a flat per-call amount. Setting both returns 400 billable_defaults_mutex.
metadataFree-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).

Resale and margin

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 prefixed ctok_. 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) and bind_ip lock 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 OpenAI file_search tool inside an Assistants run. Stores are per-customer tenant-isolated; turn on graphrag_enabled and file_search also returns a facts[] array of subject/predicate/object relationships next to chunk citations.
Webhooks
Register an HTTPS endpoint at POST /v1/webhooks and Ringside delivers signed events (customer budget exceeded, wallet.low, batch completed and more). Each delivery carries X-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_action when it calls a function tool.
Batch
Submit up to 50,000 requests against /v1/chat/completions, /v1/embeddings or /v1/moderations for half the synchronous price, with a 24h completion window.
Moderation
POST /v1/moderations meters at $0, so you can screen inline for free, or set the moderation param on a chat call to screen the prompt and/or completion.
Idempotency
Send the lowercase idempotency-key header 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_search without 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