Migrating from OpenRouter to Ringside
Your agent got stuck in a tool loop overnight. By the time anyone looked at it, it had burned through a chunk of the credit balance that the rest of your customers also draw on, and the only lever OpenRouter gave you was an account-level cap, which cuts everyone off at once or nobody. So you didn't set one. Then the invoice arrives, a customer queries their line item, and there is no per-customer number anywhere in the OpenRouter dashboard to answer them with, because from OpenRouter's side you are a single account buying tokens.
That's the shape of the gap. OpenRouter is a good multi-provider aggregator and the wire format is close enough that this migration is mostly a base_url string. What you get on the other side is a Customer record per end-user, with its own budget, its own usage query and its own webhooks.
Status: v1 (2026-04-20). See also: Migration library index.
Read this before you worry about breaking prod
The wire format is the same one you already speak. Both sides are OpenAI Chat Completions. Your request builder, your streaming parser, your error handling and your SDK all stay exactly as they are.
There is nothing to migrate. Chat Completions is stateless, so nothing of yours lives inside OpenRouter. No history to export, no keys to re-issue, no data move.
Rollback is one line. Point base_url back at https://openrouter.ai/api/v1, swap the key variable and drop the fc: prefix. That's the entire back-out and it doesn't touch anything else.
You can run both at once. Since both speak OpenAI wire format, the difference in your code is one base_url variable. Route 10% of traffic to Ringside, watch latency and cost on real requests for a week, then move the rest. Both providers stay live the whole time.
The user field is additive. Adding user: "cus_42" to a call that already works cannot break it. On OpenRouter it's ignored; on Ringside it creates the attribution. You can ship that change ahead of the cutover and nothing happens until the base URL moves.
The whole change, in one diff
Python
python# Before (OpenRouter) from openai import OpenAI client = OpenAI( api_key="sk-or-v1-...", base_url="https://openrouter.ai/api/v1", ) resp = client.chat.completions.create( model="anthropic/claude-3.5-sonnet", messages=[{"role": "user", "content": "Hello"}], ) # After (Ringside) client = OpenAI( api_key="ko_...", base_url="https://api.fightclub.pro/v1", ) resp = client.chat.completions.create( model="fc:anthropic/claude-sonnet-4-5", # add the fc: prefix messages=[{"role": "user", "content": "Hello"}], user="cus_42", # <- attribution you didn't have )
One URL swap, one fc: prefix, one user field. Done. Ringside server tokens are ko_ followed by 64 hex characters, with no _live_ or _test_ segment.
Node.js / TypeScript
javascript// Before const openai = new OpenAI({ apiKey: process.env.OPENROUTER_API_KEY, baseURL: "https://openrouter.ai/api/v1", }); // After const openai = new OpenAI({ apiKey: process.env.RINGSIDE_API_KEY, baseURL: "https://api.fightclub.pro/v1", }); const resp = await openai.chat.completions.create({ model: "fc:anthropic/claude-sonnet-4-5", messages: [{ role: "user", content: "Hello" }], user: "cus_42", });
Why move
Per-end-customer billing attribution. OpenRouter shows you a single total and has no concept of which of your end-customers caused which dollar. Building that yourself means correlating every call with your own user ID, writing a usage event, aggregating it on a schedule and putting a dashboard on top, which is a schema, a pipeline and a page you now own forever. On Ringside user: "cus_42" does it server-side, the Customer is created lazily the first time you name one, and GET /v1/customers/cus_42/usage gives you the answer when someone disputes an invoice.
Budget caps that don't take everyone down with them. OpenRouter's credit cap sits at the account level, which is why most people never set one: the failure mode is worse than the overspend. PATCH /v1/customers/cus_42 {"monthly_budget_usd": 50} puts the ceiling on one customer. Past it their calls return 402 customer_budget_exceeded and everyone else carries on. The runaway-loop story above stops at $50 instead of stopping when a human wakes up.
Webhooks. 34 registered event types, including customer.budget_exceeded, wallet.low and moderation.flagged. HMAC-signed over t.body with a 5-minute tolerance, retried at 1m, 5m, 30m, 2h and 6h. A few of the 34 are registered ahead of being emitted. OpenRouter has no webhook surface at all, so today the equivalent is a cron job re-reading state on a timer.
Client Tokens. Short-lived Ed25519-signed JWTs pinned to a Customer, with optional origin allowlist and optional IP hash, so browsers can call chat/completions directly. The backend proxy route you wrote purely to keep the key off the client goes away.
Conversation persistence. Ringside stores threads, messages and runs server-side, so chat history stops being a Postgres schema you maintain.
Margin reporting. Send X-FC-Billable-Amount: 0.25 on the call and GET /v1/margin shows what you charged against what it cost. Pricing experiments run on real numbers instead of a spreadsheet estimate.
Step-by-step (60 seconds)
- Sign up at
ringside.fightclub.pro/register?intent=ringside. $10 credit, no card. - Create a server token at
/ringside/app/api-keys/new. - Update your SDK init:
base_url = "https://api.fightclub.pro/v1",api_key = "ko_...". - Rewrite your model string:
anthropic/claude-3.5-sonnet→fc:anthropic/claude-sonnet-4-5. Ringside uses the provider's canonical current model name;GET /v1/modelsis the live list across all 19 providers. - Add
user: "<your-end-customer-id>"to every call.
Drop-in code diffs
Streaming
pythonstream = client.chat.completions.create( model="fc:openai/gpt-4o-mini", messages=[{"role": "user", "content": "Stream a poem"}], stream=True, user="cus_42", ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="", flush=True)
Ringside emits OpenAI chat-completion-chunk format byte-exact across all 19 providers (C.6 canonical SSE layer). Same shape OpenRouter emits today, so no parser change.
Tool calling with multi-provider fallback
python# Ringside slot alias, re-pointable without code changes resp = client.chat.completions.create( model="slot:chat-fast", messages=[{"role": "user", "content": "What's the weather in SF?"}], tools=[{ "type": "function", "function": { "name": "get_weather", "parameters": { "type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"], }, }, }], user="cus_42", )
Slot aliases (slot:<name>) resolve via platformSettings.slotAliases. You can swap slot:chat-fast from fc:openai/gpt-4o-mini to fc:groq/llama-3.1-70b in the dashboard without redeploying.
Attribution via headers (recommended for multi-tag setups)
pythonclient.chat.completions.create( model="fc:openai/gpt-4o-mini", messages=[{"role": "user", "content": "Hello"}], user="cus_42", extra_headers={ "FC-Tag": "support-bot", "FC-Property-Plan": "enterprise", "X-FC-Billable-Amount": "0.50", # what you charged your customer for this call }, )
Then query margin:
bashcurl -H "Authorization: Bearer ko_..." \ "https://api.fightclub.pro/v1/customers/cus_42/margin?group_by=day"
Feature mapping
| OpenRouter | Ringside equivalent |
|---|---|
openrouter.ai/api/v1/chat/completions | api.fightclub.pro/v1/chat/completions |
model: "anthropic/claude-3.5-sonnet" | model: "fc:anthropic/claude-sonnet-4-5" (add fc: prefix) |
HTTP-Referer / X-Title headers | Optional FC-Tag / FC-Property-* headers for richer metadata |
| Account-level credit cap | Per-Customer + global budget caps |
Transforms (transforms: ["middle-out"]) | Not v1, most use cases solved by max_tokens + model choice |
Auto-routing (model: "openrouter/auto") | Not v1 (v2 target, slot: aliases give you manual failover today) |
provider: {order: [...]} preference | Use explicit fc:<provider>/<model> per-call |
| Usage dashboard | GET /v1/usage, /margin, /customers/:id/usage + /ringside/app/usage UI |
| n/a | Customer Object + per-Customer budgets |
| n/a | Webhooks (34 registered event types) |
| n/a | Client Tokens (frontend-safe JWTs) |
| n/a | Conversation persistence (/v1/customers/:id/conversations) |
| n/a | Managed RAG (/v1/vector_stores + file_search) with per-store encryption |
| n/a | Batch API (POST /v1/batches, completion_window: "24h", 0.5x sync markup from 10 requests up) |
| n/a | Assistants API wire-compat (for migrating from OpenAI Assistants) |
| n/a | Margin reporting (your charge vs. Ringside cost) |
Gotchas
- Pricing shape differs. OpenRouter's markup is roughly 5% flat. Ringside's is 6% (Pro) or 10% (Developer), plus the Pro tier's $99/mo base. Compare your monthly volume against what the Customer layer would cost you to build. Rough break-even: if you'd spend an engineer-week on attribution alone, Ringside Pro is cheaper in year one.
- Model naming. Ringside uses
fc:<provider>/<current_name>. OpenRouter sometimes lags the provider's current naming, soclaude-3.5-sonneton one side isclaude-sonnet-4-5on the other. CheckGET /v1/modelsfor the canonical Ringside name before you cut over, and fix the strings in the same commit as the base URL. - No auto-routing in v1. OpenRouter's
openrouter/autoroutes to cheapest-matching. Ringside v2 will add this (§22 in the v1 API spec). For now, useslot:<alias>indirection. You pick the target; we make it re-pointable. - No provider-order preference in v1. OpenRouter lets you prefer Azure OpenAI over OpenAI direct. Ringside v1 pins each
fc:<provider>/<model>to a single upstream. v2 will add preference lists. - Transforms (middle-out and friends) not supported. The common use case, long-context truncation, is handled by Ringside's conversation-context truncation on
/v1/conversations/*/messagesplus explicitmax_tokens. HTTP-RefererandX-Titleheaders. OpenRouter uses these for its public leaderboard. Ringside ignores them; there is no public leaderboard here, by design. UseFC-Property-*if you want the metadata attached to UsageEvents.
FAQ
Q: Do you cover the models I'm using? A: Ringside routes to 19 providers behind one base URL, including Anthropic, OpenAI, Google, Mistral, Groq, Together, Fireworks, DeepSeek, xAI, Cerebras, Bedrock and Azure OpenAI. GET /v1/models is the live catalogue, and it's the only number worth trusting because it moves every week. Check your top five model strings against it before you commit; if one is missing, open a ticket and we'll add it.
Q: What about prompt caching across providers? A: OpenAI automatic caching and Anthropic cache_control blocks both work. Ringside's C.6 canonical usage layer reports cached_input_tokens uniformly.
Q: Is Ringside faster than OpenRouter? A: Comparable. Both add roughly 10-30ms of edge overhead on top of the upstream provider. Streaming first-byte is unaffected.
Q: Do I get a Customer-level dashboard? A: Yes. /ringside/app/customers/[id] shows per-Customer conversations, usage, budget status, webhooks and Client Tokens in one view.
Q: Can I run OpenRouter and Ringside side-by-side? A: Yes, and it's the recommended way to cut over. Route a percentage of traffic to each and compare. Since both speak OpenAI wire format, your client code branches on one base_url variable.
Next steps
- Try in the no-signup playground.
- Model catalog:
ringside.fightclub.pro/docs/models. - Support: open a ticket.
Related migrations
- OpenAI Chat Completions → Ringside
- OpenAI Assistants → Ringside
- Anthropic Messages → Ringside
- Chatbase → Ringside
- Index
Corrections or feedback: open an issue.