Quickstart
Go from a fresh account to a billed chat completion in five steps. You will mint a developer key, make a raw curl call, repeat it through the stock OpenAI SDK by changing one line, attribute the spend to a customer with a single header and then read the call back in the dashboard with its request id and exact cost. Every value below is real: real header names, real id prefixes and the actual response your account returns.
What you are building
Ringside is an OpenAI-compatible gateway in front of 19 model providers. You point your existing OpenAI client at https://api.fightclub.pro/v1, prefix the model name and your code keeps working. What Ringside adds on top is the metering, per-customer attribution and the dashboard ledger. Every call deducts from a prepaid wallet, every call can be tagged to a customer for resale and every call lands in the dashboard logs with a request id you can quote in a support ticket.
The mental model is two layers. The lower layer is the OpenAI surface you already know (/v1/chat/completions, the same request and response JSON). The upper layer is the set of FC-* and X-Ringside-* headers that carry the Ringside-only behaviour. You opt into the upper layer header by header, so a drop-in port costs you nothing and the billing features are there when you reach for them.
- API base URL
https://api.fightclub.pro/v1for every request.- Dashboard
https://ringside.fightclub.pro(keys at/app/api-keys, logs at/app/logs, customers at/app/customers).- Developer key
- A
ko_key, sent asAuthorization: Bearer ko_<key>. This is what you mint below. It bills your developer wallet. - Customer
- A
cus_record you attribute spend to with theFC-Customerheader. Created on first use unless you disable lazy-create.
There is no test-vs-live key distinction. A ko_ key is a ko_ key and every successful call spends real wallet balance. Top up a small amount and watch the X-Ringside-Wallet-Balance header to stay oriented while you experiment.
Step 1: Create a key and export it
- 1Mint the key
Open /app/api-keys, create a key and grant it the
api:chatscope (chat completions check for that scope on every request). The full token is shown once, at creation. It is SHA-256 hashed at rest, so we cannot show it again. Copy it now. - 2Confirm the format
A developer key is literally the string
ko_followed by 64 hex characters. If your copy is shorter, you grabbed the displayed prefix (the first 11 chars) rather than the whole token. - 3Export it
export FC_API_KEY="ko_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
| Scope | Grants |
|---|---|
api:chat | Chat completions and the other inference endpoints. Required for this quickstart. |
api:read | Read customers, logs, usage and other GET routes. |
api:write | Create and mutate customers, vector stores and the like. |
api:webhooks | Manage webhook endpoints. |
api:client_tokens | Mint browser-safe Client tokens via POST /v1/customers/{id}/client_tokens. |
A ko_ developer key spends your wallet and carries whatever scopes you granted it. Never ship one to a browser or mobile app. For client-side use, mint a short-lived Client token instead (see authentication).
Step 2: Your first call with curl
The only Ringside-specific thing here is the model ref. A bare name like gpt-4o is rejected with 400 invalid_model_ref. Every model needs a prefix; the simplest is fc:<provider>/<model> for a pinned model. We use fc:openai/gpt-4o-mini.
curl https://api.fightclub.pro/v1/chat/completions \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "fc:openai/gpt-4o-mini",
"messages": [
{ "role": "user", "content": "In one sentence, what is a tarpit?" }
]
}'The body is the standard OpenAI envelope. The id carries a chatcmpl- prefix and the usage object reports tokens exactly as OpenAI does.
{
"id": "chatcmpl-9f3a1c7d8e2b4a6c1f0d5e2a",
"object": "chat.completion",
"created": 1750800000,
"model": "fc:openai/gpt-4o-mini",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "A tarpit is a service that deliberately answers slowly to waste an attacker's time and tie up their connections."
},
"finish_reason": "stop",
"logprobs": null
}
],
"usage": {
"prompt_tokens": 18,
"completion_tokens": 24,
"total_tokens": 42
}
}Most of the Ringside metadata rides in the response headers rather than the body. Run the same call with curl -i (or -D -) and you get the metering and routing headers. The two you will watch most are the wallet headers.
| Header | Meaning |
|---|---|
X-Request-Id | The req_ id for this call. It is a header, never a body field. Quote it in support tickets and use it to find the call in the logs. |
X-Ringside-Wallet-Balance | Wallet balance after this call, in dollars. Present when the call attributed to a customer wallet; otherwise read your developer-wallet balance in the dashboard. |
X-Ringside-Wallet-Deducted | The exact charge for this call, in dollars. |
X-Ringside-Model-Resolved | The concrete fc: model a match: / slot: / dyn: ref resolved to. Identity for a pinned fc: ref. |
X-RateLimit-Remaining | Requests left in the current window. There is no X-RateLimit-Limit or X-RateLimit-Reset. |
Step 3: The same call through the OpenAI SDK
Because the surface is OpenAI-compatible, the only edits to existing SDK code are the base URL, the key and the model prefix. No Ringside SDK required for plain inference. Set base_url (Python) or baseURL (Node) to https://api.fightclub.pro/v1 and pass your ko_ key as the API key; the SDK puts it in the Authorization: Bearer header for you.
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["FC_API_KEY"],
base_url="https://api.fightclub.pro/v1",
)
resp = client.chat.completions.create(
model="fc:openai/gpt-4o-mini",
messages=[{"role": "user", "content": "In one sentence, what is a tarpit?"}],
)
print(resp.choices[0].message.content)The OpenAI SDKs expose raw responses so you can still read the X-Ringside-* headers. In Python use client.chat.completions.with_raw_response.create(...) and read .headers; in Node use client.chat.completions.create(...).asResponse(). For typed helpers around the Ringside-only primitives (the match: builder, customers CRUD, webhook signature verification) install the official SDK: pip install ringside or npm install @ringside/sdk.
Step 4: Attribute the spend to a customer
Add one header and the call is billed against a customer rather than sitting as undifferentiated developer spend. FC-Customer accepts a cus_ id, an ext:<external_id> reference or a plain external id string. If the customer does not exist yet, Ringside lazy-creates it on the spot, so you can start tagging by your own user ids before you have created a single customer record.
curl https://api.fightclub.pro/v1/chat/completions \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Content-Type: application/json" \
-H "FC-Customer: ext:acme-corp" \
-d '{
"model": "fc:openai/gpt-4o-mini",
"messages": [
{ "role": "user", "content": "In one sentence, what is a tarpit?" }
]
}'On the first call with FC-Customer: ext:acme-corp, Ringside creates a customer (say cus_42) with that external id and attributes the spend to it. The customer now appears under /app/customers and the response carries X-Ringside-Wallet-Balance / X-Ringside-Wallet-Deducted for that customer wallet. Subsequent calls with the same value resolve to the same cus_42 with no extra round trip.
| Header | Value | Effect |
|---|---|---|
FC-Customer | cus_42, ext:acme-corp or acme-corp | Attributes the call. Lazy-creates the customer if unknown. |
FC-Customer-Strict | true | Disables lazy-create. An unknown id then returns 404 not_found_error instead of creating a record. |
X-FC-Billable-Amount | integer micro-dollars | Override the metered charge (resale / markup). Margin is billable - raw cost. |
A customer can carry a monthly_budget_usd; once spent, calls return 402 customer_budget_exceeded. If the customer holds a prepaid wallet and it is empty, you get 402 customer_wallet_empty. With no customer attribution, an empty developer wallet returns 402 wallet_empty. Branch on the code field, never the human-readable message.
Step 5: Find the call in the dashboard
- 01Grab the
X-Request-Idvalue from the response headers (for examplereq_8c2f...). - 02Open /app/logs.
- 03Find the row for that request id. It shows the resolved model, the customer it was attributed to, the token usage and the exact cost (the same figure as
X-Ringside-Wallet-Deducted). - 04Click through to the full request / response detail to confirm the attribution landed on
cus_42.
If a call is missing from the logs, it almost always failed before metering. Re-read the response: an auth failure (Step 6) or a 400 validation error never reaches the billing path and so never produces a log row. A successful 200 always does.
Failure modes you will hit first
Every error shares one shape. type, code and message are always present; param appears on field-validation errors and retry_after_seconds on rate-limit errors. Write your handling against code.
{
"error": {
"type": "invalid_request_error",
"code": "invalid_model_ref",
"message": "Model ref must start with 'fc:', 'slot:', or 'match:'. Got: gpt-4o",
"param": "model"
}
}| Status | code | Cause |
|---|---|---|
| 401 | missing_token | No Authorization header. |
| 401 | invalid_auth_scheme | The scheme is not Bearer or Client . Message: "Authorization header must start with 'Bearer ' or 'Client '". |
| 401 | invalid_token_format | Bearer token does not start with ko_. |
| 401 | invalid_token | Key is unknown, revoked or expired. |
| 403 | insufficient_scope | The key lacks api:chat. Re-mint with that scope. |
| 400 | invalid_model_ref | Model is a bare name. Add an fc: / match: / slot: / dyn: prefix. |
| 400 | missing_messages | messages is absent or empty. |
| 402 | wallet_empty | Developer wallet has no balance. Top up. |
| 404 | not_found_error | Cross-tenant id, or an unknown FC-Customer under FC-Customer-Strict: true. We return 404 (not 403) so ids are not enumerable. |
| 429 | rate_limit_error | A rate-limit layer tripped. Read Retry-After (integer seconds) and retry_after_seconds in the body. |
To make a retry idempotent, send an idempotency-key header (lowercase, that exact spelling). The replay window is 24 hours, scoped to (developer, customer, key). One caveat: you cannot combine an idempotency-key with a model array ("model": [...]) cost cascade; that returns 400 fallback_with_idempotency_unsupported.
Where to go next
Developer keys vs browser `Client` tokens, scopes, the `Bearer` / `Client` schemes and token minting.
First-class `cus_` records: budgets, rpm / tpm limits, prepaid wallets, markup defaults and `ext:` addressing.
Every body param, header and error for `POST /v1/chat/completions`, including streaming and model cascades.