Prefer Swagger UI? Click hereThe same API in the classic OpenAPI explorer.
// concept · 9 min read

Metering and billing

Every Ringside call moves money through two ledgers. The raw provider cost becomes a charge against your developer wallet, and an optional billable amount becomes a debit against an attributed customer wallet. This page traces one call end to end: how the charge is computed, which wallet it hits, what the response headers report, how margin and usage rollups are derived and what you get back at zero balance.

The mental model

Ringside sits between you and 19 upstream providers, so every call has a real upstream cost the moment it completes. Billing turns that cost into ledger entries. There are two ledgers, and they are independent.

Developer wallet (the API pool)
A single prepaid USD balance on your Ringside account. Every Ringside call debits it by the *billable cost* (raw provider cost plus platform markup). This is the money Ringside collects from you. It is one shared pot, not fenced per surface.
Customer wallet
A separate prepaid USD balance you can enable per customer (cus_). When a call is attributed to a customer, Ringside debits this wallet by the *billable amount* you set for resale. This is the money you collect from your end customer. It is tracked independently in the customer ledger and never mixes with your developer wallet.

The gap between the two is your margin. You pay Ringside cost_charged_usd out of the developer wallet; you bill your customer billable_amount_usd out of their customer wallet; the difference is what you keep. Both deductions happen on the same call, against two different balances.

provider raw cost  (costRaw)
        |
        |  x (1 + markupPct)  x batchMult
        v
  cost_charged_usd  -----debit----->  DEVELOPER WALLET (api_charge)
        |
        |  resolveBillableAmount(header, defaults)
        v
  billable_amount_usd  --debit-->  CUSTOMER WALLET (debit)   [only if attributed]
        |
        v
  margin_usd = billable_amount_usd - cost_charged_usd
One call, two ledgers. The customer-wallet leg only runs when the call carries an FC-Customer attribution and a positive billable amount.

From a call to a charge

Take a single POST /v1/chat/completions. Once the upstream provider returns, Ringside computes the cost from real token counts, not an estimate.

costRaw
The unmarked-up provider cost, computed from prompt_tokens, completion_tokens and any cache-creation / cache-read tokens at the provider price. Recorded on the usage event as cost_raw_usd.
markupPct
The platform markup resolved for the model: a per-model override, else a per-tier default (frontier / mid / budget), else a price-bucketed global fallback. DeepSeek models carry an extra non-reclaimable +10% VAT uplift on top of the resolved markup.
batchMult
A multiplier of 1 for synchronous calls and 0.5 for batch-child rows (batch runs at half the synchronous price). A cache hit on a batch child compounds to 0.05x.
cost_charged_usd
The billable cost: costRaw * (1 + markupPct) * batchMult. This exact number is deducted from your developer wallet and recorded on the usage event as cost_charged_usd.

The deduction is atomic. Ringside runs a conditional UPDATE ... WHERE wallet_balance >= amount, so a call either debits the full amount or changes nothing. On success it writes one ledger row of type api_charge with the amount stored as a negative number and balance_after snapshotted. Zero rows back means the balance was insufficient and you get a 402.

Resolving the billable amount

The billable amount is what you charge your customer, and it is resolved in a strict precedence order. The first rule that produces a value wins.

  1. 01The X-FC-Billable-Amount request header, if present. Its value is a plain non-negative USD number (for example 0.02). A negative or non-finite value is rejected up front with 400 invalid_billable_amount.
  2. 02Else the customer's default_billable_markup_pct, applied as cost_charged_usd * (1 + pct/100).
  3. 03Else the customer's default_billable_amount_usd, used as a flat per-call price.
  4. 04Else null. With no header and no customer default the call has no billable amount, so no customer-wallet debit happens.
markup vs flat are mutually exclusive

A customer can hold default_billable_markup_pct OR default_billable_amount_usd, never both. Setting both on a create or update returns 400 billable_defaults_mutex. Pick percentage markup for cost-plus resale, or a flat amount for a fixed per-call price.

Attributing a call to a customer

Attach the FC-Customer header to route the billable leg to a customer wallet. The value can be a cus_ id, an ext:<external_id> reference or a plain external id string. Without this header the call still debits your developer wallet; it just has no customer leg.

curl https://api.fightclub.pro/v1/chat/completions \
  -H "Authorization: Bearer ko_<key>" \
  -H "Content-Type: application/json" \
  -H "FC-Customer: cus_42" \
  -H "X-FC-Billable-Amount: 0.02" \
  -d '{
    "model": "fc:openai/gpt-4o-mini",
    "messages": [{"role":"user","content":"ping"}]
  }'
The customer-wallet debit is best-effort

The chat response is built and returned first; the customer-wallet debit fires after. A debit failure (for example the wallet was never enabled) never blocks or fails the completion. The developer-wallet charge, by contrast, is on the critical path: if it cannot be taken, the call is refused with 402.

Per-call response headers

When a call carries a customer attribution and a positive billable amount, the response reports the customer wallet state. Both values are formatted to 7 decimal places.

Emitted on success only when the call is attributed and the billable amount is greater than zero. A direct (unattributed) call, or one with a null/zero billable amount, returns neither header.
HeaderMeaning
X-Ringside-Wallet-BalanceThe customer wallet balance in USD after this call's debit.
X-Ringside-Wallet-DeductedThe exact amount debited from the customer wallet for this call (the resolved billable_amount_usd).
These headers report the CUSTOMER wallet, not your API pool

X-Ringside-Wallet-Balance is the attributed customer's remaining prepaid balance, not your developer pool. To watch your own pool, read the balance off your account or subscribe to the wallet.low and wallet.empty webhooks (the low threshold fires once on the downward crossing of $5).

On a 402 caused by an empty or short customer wallet, Ringside still returns X-Ringside-Wallet-Balance so you can see exactly how much was left when the call was refused.

The wallet ledgers

Both wallets are append-only ledgers. The balance column is the source of truth; every mutation also writes a transaction row with the post-mutation balance snapshotted, so you can audit any balance back to zero. The two ledgers use different transaction-type vocabularies.

The developer wallet is one shared pot across Fight Club, Knockout and Ringside, so its ledger carries charge types from all three surfaces. Ringside calls always use `api_charge`.
LedgerTransaction typesSign convention
Customer wallettopup, debit, adjust, refundCredits (topup, positive adjust) are positive; debit is stored negative.
Developer wallet (API pool)topup, api_charge, refund (plus fight_charge, ko_session_charge, ko_snapshot_charge from other surfaces)Every charge is stored as a negative amount; topup is positive.

Funding and adjusting a customer wallet

A customer wallet starts disabled (balance NULL). The first topup (or adjust) initializes it. Until then an attributed call that resolves a positive billable amount will fail its debit with a wallet-not-enabled condition, which is swallowed and does not affect the completion.

EndpointEffectLedger typeConstraints
POST /v1/customers/{id}/wallet/topupAdds a positive amount; initializes the wallet from 0 if it was NULL.topupamount must be a positive number; magnitude <= $1,000,000.
POST /v1/customers/{id}/wallet/adjustApplies an arbitrary positive or negative correction.adjustamount must be non-zero and finite; magnitude <= $1,000,000.
GET /v1/customers/{id}/walletReads the current balance and enabled state.(read)Returns enabled: false with balance 0 when never funded.
curl https://api.fightclub.pro/v1/customers/cus_42/wallet/topup \
  -H "Authorization: Bearer ko_<key>" \
  -H "Content-Type: application/json" \
  -d '{"amount": 50, "description": "June prepay"}'
Tip

Every customer-wallet debit row carries the originating request_id, so you can join a ledger line straight back to the request log entry and the usage event that produced it. topup and adjust rows have a null request id.

A worked example

You resell fc:openai/gpt-4o-mini. A single synchronous call to customer cus_42 runs 1,000 prompt tokens and 400 completion tokens. Suppose the provider raw cost lands at $0.000310 and your platform markup for that model is 20%.

QuantityValueHow it is derived
cost_raw_usd$0.00031000Provider price applied to the real token counts.
markupPct0.20Resolved markup for the model (override, tier or fallback).
batchMult1Synchronous call (0.5 if this were a batch child).
cost_charged_usd$0.000372000.00031 * (1 + 0.20) * 1 -> debited from your developer wallet as an api_charge.
X-FC-Billable-Amount0.02You set a flat 2-cent resale price on the request header.
billable_amount_usd$0.02000000Header value wins over any customer default -> debited from cus_42 as a debit.
margin_usd$0.019628000.02 - 0.000372. Your gross margin on this one call.

After the call, cus_42's wallet (funded to $50.00) sits at $49.98, and the response carries the headers below. Your developer wallet drops by $0.000372, recorded as a negative api_charge row.

X-Request-Id: req_5d3b8a1c9e2f4a6b7c0d1e2f
X-Ringside-Model-Resolved: fc:openai/gpt-4o-mini
X-Ringside-Wallet-Balance: 49.9800000
X-Ringside-Wallet-Deducted: 0.0200000
X-RateLimit-Remaining: 59

Had you sent no X-FC-Billable-Amount header and instead set default_billable_markup_pct: 30 on cus_42, the billable amount would resolve to cost_charged_usd * 1.30 = $0.00048360, the debit and the X-Ringside-Wallet-Deducted header would carry that value, and your margin would shrink to roughly $0.00011 on the call.

Margin and usage rollups

GET /v1/customers/{id}/margin

Margin reporting compares what you billed against what the traffic cost you over a window. It sums billable_amount_usd (revenue) and cost_charged_usd (cost) over status=ok usage events, then derives margin_usd = revenue - cost per bucket and as a total. Blocked and errored events carry neither revenue nor cost, so they never distort the report.

Query paramDefaultNotes
group_bydayOne of day, customer, model. Anything else returns 400 invalid_group_by.
from / tolast 30 daysISO 8601 timestamps. An unparseable value returns 400 invalid_date with the offending param.
curl "https://api.fightclub.pro/v1/customers/cus_42/margin?group_by=day" \
  -H "Authorization: Bearer ko_<key>"
from ringside import Ringside

client = Ringside(api_key="ko_<key>")
report = client.customers.margin("cus_42", group_by="day")
print(report.total.margin_usd, report.total.margin_pct)

GET /v1/customers/{id}/usage

Usage rollups aggregate cost and token counts (no revenue) for one customer. Use them to see where a customer's spend lands by day, model, tag, custom property or conversation. The llm_cost_usd field is the sum of cost_charged_usd, that is, what the traffic cost you including markup.

Query paramDefaultNotes
group_bydayOne of day, model, tag, property, conversation. Invalid values return 400 invalid_group_by.
property_key(none)Required when group_by=property, else 400 missing_property_key.
tag(none)Optional pre-filter to a single FC-Tag value before aggregating.
from / tolast 30 daysISO 8601; unparseable -> 400 invalid_date.

Each bucket reports llm_cost_usd, input_tokens, output_tokens and request_count. The total block adds cached_input_tokens, reasoning_tokens, message_count (conversation messages) and conversation_count. The platform_cost_usd field is reserved and currently 0.

Usage shows cost, margin shows revenue

Usage rollups never include billable_amount_usd. If you want what you billed (and your margin), call the margin endpoint. If you want what the traffic cost you and the token breakdown, call usage. They read the same underlying usage events from different angles.

Zero balance and other billing failures

Billing has four distinct 402 failures plus the budget gate. Branch on the code, never the human message. The four 402s and the budget gate all carry type: "payment_required". The billable_defaults_mutex config error is a 400 with type: "invalid_request_error". Every one rides the standard error envelope, with the request id in the X-Request-Id response header rather than the body.

The two customer-wallet 402s carry X-Ringside-Wallet-Balance so you can read the remaining balance off the failed response.
codeHTTPTriggerWhich balance
wallet_empty402Your developer wallet (API pool) is at or below zero, or the atomic charge could not be taken.Developer wallet
customer_wallet_empty402The attributed customer wallet balance is zero or below at the pre-call gate.Customer wallet
customer_wallet_insufficient402The customer wallet has a positive balance, but less than the estimated billable cost of this call.Customer wallet
customer_budget_exceeded402The customer's monthly_budget_usd is already met by this month's spend.Budget cap (not a wallet)
billable_defaults_mutex400A customer create/update set both default_billable_markup_pct and default_billable_amount_usd.(config)

Order of checks matters. For an attributed call Ringside gates the customer wallet first (and the monthly budget, if set), then checks your developer API pool, then runs the call. So a customer with an empty wallet is refused before your pool is ever touched. The customer-wallet check is an estimate based on the request's max_tokens (or a modest default), which keeps cheap calls from being over-rejected at the cost of a bounded one-call overshoot.

{
  "error": {
    "type": "payment_required",
    "code": "customer_wallet_empty",
    "message": "Customer cus_42 has no remaining wallet balance"
  }
}
Handle 402 by topping up, not retrying

A 402 is deterministic: the same call will keep failing until the balance moves. On customer_wallet_empty or customer_wallet_insufficient, call wallet/topup for that customer. On wallet_empty, top up your own Ringside balance. Subscribe to wallet.low, wallet.empty and customer.budget_exceeded webhooks so you act before a call is refused.

Reconciliation

Because both ledgers snapshot a balance on every row and tag every customer debit with a request id, reconciliation is mechanical.

  • ·Tie revenue to the ledger: sum the customer wallet debit rows over a window and compare to the margin report's billable_amount_usd total for the same customer and window. They should match.
  • ·Tie cost to the ledger: sum your developer wallet api_charge rows and compare to the usage report's llm_cost_usd total. Both are cost_charged_usd.
  • ·Trace a single call: take the request_id from a customer debit row and look it up in the request log to see tokens, model, status and the matching usage event.
  • ·Account for replays: an idempotent retry (same idempotency-key) is a replay. It returns the stored response and does NOT re-charge either wallet, so it produces no second ledger row. Do not count it as new spend.
  • ·Account for non-ok events: blocked-budget, moderation-flagged and errored events record cost_charged_usd and billable_amount_usd of 0 and never debit. Margin and usage rollups already exclude them, but the raw request log still lists them.
  • ·Watch the sign: charges and debits are stored negative in their respective ledgers, top-ups and positive adjustments are positive. Sum with the sign intact, or take absolute values consistently.
The two wallets do not net against each other

A customer-wallet debit failing (wallet disabled, race) does not roll back the developer-wallet charge for that call: you still paid Ringside, you just did not collect from the customer. Reconcile each ledger on its own, then compare the totals to spot any call where you ate the cost without billing it.