Cost control
Ringside meters every call against a prepaid wallet the moment it completes, so spend is a function of three things you control: which model resolves, how much of each prompt you pay full price for and whether the work runs interactively or in batch. This page is the operator playbook. Set hard ceilings per customer, start on the cheapest model that can do the job and only escalate, cache the stable parts of your prompts, push large non-interactive jobs to the half-price batch lane and wire the wallet.low webhook so you hear about a drain before it becomes a 402.
The mental model
There is no monthly invoice to reconcile after the fact. Ringside is prepaid. Each call debits a USD balance at completion and the response tells you the exact charge and the balance left, so cost control is something you do up front and in real time, not in a spreadsheet at month end.
Two balances are in play on every call. Your developer wallet (the shared API pool, one pot across Fight Club, Knockout and Ringside) is debited by the billable cost: raw provider cost plus platform markup. When a call is attributed to a customer that has a prepaid wallet enabled, that customer wallet is debited instead by the billable amount you set for resale. The difference between what you bill the customer and what Ringside charges you is your margin. Every lever on this page either lowers the raw cost (so your wallet drains slower) or caps how fast a customer can spend (so a runaway integration cannot bankrupt you).
The developer wallet is read by getApiPoolBalance and is a single shared balance. Customer wallets are separate, enabled per customer, and a null balance means the wallet is not enabled (calls then fall through to your developer wallet). See Metering and billing for the full two-ledger model.
The levers, ranked by saving
Reach for these top to bottom. The first two are guardrails that bound the worst case. The rest cut the actual per-call cost.
| Lever | How | Typical saving |
|---|---|---|
| Cap per-customer spend | Set monthly_budget_usd on the customer, or fund a prepaid wallet via /wallet/topup | Bounds the worst case to a known number; over budget returns 402 customer_budget_exceeded |
| Start cheap, escalate on miss | Put a cheap model first in a model:[] cascade so a bigger model is only tried on failure | Most traffic stays on the cheap tier; you pay for the large model only on the fallback |
| Batch large non-interactive jobs | Upload a JSONL file and POST /v1/batches with completion_window:"24h" | Half the synchronous per-call price |
| Cache stable prefixes | prompt_cache:"auto" or cache_control {type:ephemeral} on Anthropic-routed models | Cached input reads bill ~10% of input price |
| Skip inline moderation when you do not need it | moderation defaults to none; call free POST /v1/moderations out of band instead | Avoids a second model pass per chat call; /v1/moderations meters at $0 |
| Pin a small model outright | Use fc:openai/gpt-4o-mini instead of a large pinned model where quality allows | Pays the cheap model rate on every call, no resolution overhead |
Cap spend per customer
A customer (cus_) is the unit you put ceilings on. Two independent mechanisms, and you can run both at once.
- monthly_budget_usd
- A rolling monthly ceiling. Once the customer has spent past it, attributed calls return
402 customer_budget_exceededuntil the window rolls. Set it at create time or PATCH it later. Leave it null for no monthly cap. - Prepaid wallet
- A hard balance. Fund it with
POST /v1/customers/{id}/wallet/topup, correct it with/wallet/adjust. When attributed calls drain it to zero the next call returns402 customer_wallet_empty. A wallet that was never funded reads as not-enabled (nullbalance) and does not gate spend. - rpm_limit / tpm_limit
- Per-customer request- and token-per-minute caps. Not a dollar cap, but they throttle the rate at which any single customer can burn money, which contains a buggy or abusive client.
Pick the model that matches how you sell. Postpaid resale (you invoice monthly) maps to monthly_budget_usd. Prepaid resale (the customer buys credit) maps to the wallet. Topups and debits are atomic and every mutation writes a ledger row you can page through with GET /v1/customers/{id}/wallet (cursor pagination, limit default 20 max 100).
curl https://api.fightclub.pro/v1/customers \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{
"external_id": "acct_8831",
"display_name": "Northwind Trading",
"monthly_budget_usd": "250.00",
"rpm_limit": 120,
"default_billable_markup_pct": "30"
}'A customer carries either default_billable_markup_pct or default_billable_amount_usd, never both. Sending both returns 400 billable_defaults_mutex. Markup multiplies the raw cost; amount is a flat per-call charge. Override either on a single call with the X-FC-Billable-Amount request header (value in micro-dollars).
Start cheap, escalate only on failure
The single biggest knob is which model actually runs. Hard-pinning a large model for every call is the expensive default. The cheaper pattern resolves a small model first and only reaches for a bigger one when the small one fails. Pass an array to model and Ringside walks it left to right, charging only for the attempt that succeeds.
curl https://api.fightclub.pro/v1/chat/completions \
-H "Authorization: Bearer ko_<key>" \
-H "FC-Customer: cus_42" \
-H "Content-Type: application/json" \
-d '{
"model": ["fc:openai/gpt-4o-mini", "match:anthropic/sonnet>=4"],
"messages": [{ "role": "user", "content": "Classify this ticket: ..." }]
}'The response headers tell you exactly what happened so you can watch the escalation rate. X-Ringside-Model-Used is the model that produced the answer, X-Ringside-Models-Tried lists everything attempted and X-Ringside-Fallback-Triggered flips on when the cheap model was skipped. If Fallback-Triggered is true on a large share of traffic, the cheap tier is undersized for the job and you are paying the escalation tax constantly, so re-tune the first entry.
A model:[] cascade rejects stream:true with 400 fallback_with_stream_unsupported, rejects a response_format json_schema with 400 streaming_with_schema_fallback_unsupported and rejects an idempotency-key with 400 fallback_with_idempotency_unsupported. For streamed or idempotent calls, pin a single model.
Note the model-ref grammar: every ref needs a prefix. fc: pins, match: resolves a family (match:anthropic/sonnet>=4), slot: is your own alias and dyn: is a dynamic profile. A bare name like gpt-4o returns 400 invalid_model_ref. See Model references for the full grammar and the @region suffixes.
Cache the stable prefix
If every call shares a long fixed preamble (a system prompt, a tool schema, a few-shot block, a retrieved document) you are paying full input price to re-send identical tokens. Prompt caching makes the upstream provider charge a fraction for the repeated prefix. On Ringside this is Anthropic-routed models only, and it is off by default.
- ·
prompt_cache:"auto"places one breakpoint on your last system message. The simplest switch. - ·
cache_control {type:ephemeral, ttl 5m|1h}on a specific message gives you explicit control over where the breakpoint sits and how long the entry lives. - ·The minimum cacheable prefix is ~4,000 tokens. Below that nothing caches and you see no saving.
- ·A cache write costs ~1.25x the input price once; every subsequent cached read bills ~10% of input price. The break-even is a couple of reuses.
curl https://api.fightclub.pro/v1/chat/completions \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{
"model": "fc:anthropic/claude-sonnet",
"prompt_cache": "auto",
"messages": [
{ "role": "system", "content": "<~6k tokens of fixed instructions>" },
{ "role": "user", "content": "Today: summarize order #8831" }
]
}'Confirm it is working by reading the usage object on the response. A cache write shows cache_creation_input_tokens, a hit shows cache_read_input_tokens. If reads stay at zero across calls that share a prefix, your prefix is under the ~4,000-token floor or it is not byte-identical (a moving timestamp early in the prompt invalidates everything after it). Keep volatile content after the cached block. Full detail in Prompt caching.
Move bulk work to batch
Anything that does not need an answer this second belongs in batch: overnight enrichment, backfills, eval runs, bulk classification, embedding a corpus. Batch bills at half the synchronous per-call price. The tradeoff is latency, the completion window is 24h.
- 01Build a JSONL file where each line is one request body, then upload it with
purpose=batch. You get afile_id. - 02Create the batch:
POST /v1/batcheswithinput_file_id,endpoint(one of/v1/chat/completions,/v1/embeddings,/v1/moderations) andcompletion_window:"24h". - 03Poll
status(validating -> in_progress -> finalizing -> completed). Up to 50,000 requests per batch. - 04Download
output_file_idviaGET /v1/files/{id}/content. Failed lines land inerror_file_id.
curl https://api.fightclub.pro/v1/batches \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{
"input_file_id": "file_9c2a...",
"endpoint": "/v1/chat/completions",
"completion_window": "24h",
"metadata": { "job": "nightly-enrich" }
}'Batch and the cheap-first cascade compose: put a match: first entry and a model:[] array inside each JSONL line so the bulk job also starts on the cheap tier. See Batch processing.
Watch usage and margin
You cannot tune what you do not measure. Three reads cover it. Per-call response headers report the live charge, the GET /v1/usage rollup reports volume over a window and the margin report tells you whether your resale pricing actually clears a profit.
| Signal | Where | Tells you |
|---|---|---|
X-Ringside-Wallet-Deducted | Chat response header | The exact charge for this one call (billable cost) |
X-Ringside-Wallet-Balance | Chat response header | Developer wallet balance immediately after the deduction |
X-Ringside-Model-Used / -Models-Tried | Chat response header | Whether the cheap tier held or the cascade escalated |
GET /v1/usage | Usage rollup | Call counts and spend over a window, your developer-side view |
GET /v1/customers/{id}/margin | Margin report | billable_amount_usd minus cost_charged_usd per bucket (day, customer or model) |
The margin report is the one to watch when you resell. It sums billable (revenue) against cost (what Ringside charged you) over a default 30-day window, grouped by day, customer or model. Only status=ok events count, blocked and errored calls carry neither revenue nor cost. margin_pct is null when revenue is zero rather than dividing by zero, so a null bucket means you ran calls you did not bill for. That is usually a missing FC-Customer header or a markup of zero, both worth catching early.
curl "https://api.fightclub.pro/v1/customers/cus_42/margin?group_by=model" \ -H "Authorization: Bearer ko_<key>"
Alert before you hit zero
A drained wallet is not a silent failure, it is a 402 on the next call. Catch it before that. Register a webhook and Ringside notifies you on the way down.
- wallet.low
- Fires once when your developer wallet balance crosses from above $5 to at or below $5 on a deduction. The fixed $5 threshold is enforced post-commit so the event reflects a real balance, and it does not re-fire on every subsequent call below the line. Payload carries
balance_usdandthreshold_usd. - wallet.empty
- Fires when a deduction takes the balance to zero or below. The next call will return
402 wallet_empty(developer pool) or402 customer_wallet_empty(attributed customer wallet) until you top up. - customer.budget_exceeded
- Fires when an attributed call is rejected for crossing a customer
monthly_budget_usd. Pair it withcustomer.rate_limit_exceededto see which customers are pushing their ceilings.
curl https://api.fightclub.pro/v1/webhooks \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/ringside",
"events": ["wallet.low", "wallet.empty", "customer.budget_exceeded"]
}'Every delivery carries X-FC-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of the raw body keyed by the endpoint secret (whsec_, shown once on create). Verify constant-time and reject stale timestamps. During a secret rotation the header carries two v1= digests, accept if either matches. Detail in Webhooks and events.
Handle the spend errors
When a ceiling bites you get a structured error. Always branch on code, never on the human message, and read X-Request-Id from the response header for support. Cross-tenant access returns 404, so a customer id that is not yours looks like it does not exist rather than telling an attacker it does.
| Status | code | type | What to do |
|---|---|---|---|
| 402 | wallet_empty | wallet_error | Developer wallet is dry. Top up your account balance, then retry. |
| 402 | customer_wallet_empty | wallet_error | The attributed customer prepaid wallet is dry. Call /wallet/topup for that customer, or fall back to your developer wallet by dropping the wallet requirement. |
| 402 | customer_budget_exceeded | wallet_error | The customer hit monthly_budget_usd. Raise the budget via PATCH, or wait for the window to roll. |
| 400 | billable_defaults_mutex | invalid_request_error | You set both billable defaults on the customer. Send only one. |
| 400 | fallback_with_idempotency_unsupported | invalid_request_error | You combined a model:[] cascade with idempotency-key. Pin a single model for idempotent calls. |
| 429 | rate_limit_error (type) | rate_limit_error | A per-customer rpm_limit/tpm_limit or a higher layer bit. Read Retry-After (integer seconds) and back off. |
A retry note: the four rate-limit layers are edge per-IP, per-client-token, per-developer and per-customer, lowest wins. On a 429 the only timing header is Retry-After. X-RateLimit-Remaining rides on successful responses so you can throttle yourself before you trip the limit. There is no X-RateLimit-Limit or X-RateLimit-Reset.
A sane default setup
- 01Create each customer with a
monthly_budget_usdand either a markup or a flat billable default. Setrpm_limitto contain runaway clients. - 02Default your chat calls to a
match:cheap-firstmodel:[]cascade. Reserve hard-pinned large models for the calls that genuinely need them. - 03Turn on
prompt_cache:"auto"anywhere an Anthropic-routed call repeats a >4,000-token prefix. - 04Route every non-interactive bulk job through
/v1/batchesfor the half-price rate. - 05Subscribe to
wallet.low,wallet.emptyandcustomer.budget_exceeded, and checkGET /v1/customers/{id}/marginweekly so resale pricing never quietly goes underwater.