Ringside · Migration guide

Persona · Solo-Dev Sam

Updated 2026-07-18

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)

  1. Sign up at ringside.fightclub.pro/register?intent=ringside. $10 credit, no card.
  2. Create a server token at /ringside/app/api-keys/new.
  3. Update your SDK init: base_url = "https://api.fightclub.pro/v1", api_key = "ko_...".
  4. Rewrite your model string: anthropic/claude-3.5-sonnetfc:anthropic/claude-sonnet-4-5. Ringside uses the provider's canonical current model name; GET /v1/models is the live list across all 19 providers.
  5. Add user: "<your-end-customer-id>" to every call.

Drop-in code diffs

Streaming

python
stream = 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)

python
client.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:

bash
curl -H "Authorization: Bearer ko_..." \ "https://api.fightclub.pro/v1/customers/cus_42/margin?group_by=day"

Feature mapping

OpenRouterRingside equivalent
openrouter.ai/api/v1/chat/completionsapi.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 headersOptional FC-Tag / FC-Property-* headers for richer metadata
Account-level credit capPer-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: [...]} preferenceUse explicit fc:<provider>/<model> per-call
Usage dashboardGET /v1/usage, /margin, /customers/:id/usage + /ringside/app/usage UI
n/aCustomer Object + per-Customer budgets
n/aWebhooks (34 registered event types)
n/aClient Tokens (frontend-safe JWTs)
n/aConversation persistence (/v1/customers/:id/conversations)
n/aManaged RAG (/v1/vector_stores + file_search) with per-store encryption
n/aBatch API (POST /v1/batches, completion_window: "24h", 0.5x sync markup from 10 requests up)
n/aAssistants API wire-compat (for migrating from OpenAI Assistants)
n/aMargin 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, so claude-3.5-sonnet on one side is claude-sonnet-4-5 on the other. Check GET /v1/models for 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/auto routes to cheapest-matching. Ringside v2 will add this (§22 in the v1 API spec). For now, use slot:<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/*/messages plus explicit max_tokens.
  • HTTP-Referer and X-Title headers. OpenRouter uses these for its public leaderboard. Ringside ignores them; there is no public leaderboard here, by design. Use FC-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

Related migrations


Corrections or feedback: open an issue.