SaaS with metered AI
A buffet works because most people eat one plate. You price the room off the average and the occasional person who eats six is quietly paid for by everyone else. Then you bolt an AI feature onto a $49 plan and meet the software version of that person: one account pushing a 200-page document through a frontier model every hour, all month, for the same $49. Your revenue per user is a number you chose. Your cost per user is a number your users choose.
This page is for whoever owns that gross margin line. Usually a founder or the engineer who added the feature and now gets asked, once a month, why the model bill moved.
Follow one request
A user clicks Summarise. Your app builds a prompt, sends it, gets tokens back and bills nobody, because billing already happened when they subscribed. The provider counts input tokens, output tokens and any cached prefix, adds it to a running total for your one API key, and thirty days later hands you a single invoice for that key. Everything that happened in between has been averaged into one number.
That is the whole problem. Attribution is destroyed at the moment of the call, not at the end of the month. No amount of log-mining afterwards gets it back, because the provider never knew which of your users the request belonged to. If you want per-user cost, the user has to be attached to the request while the request is happening. On Ringside that is one header, FC-Customer, and every downstream number keys off it.
Why the nightly job never works
Nearly every team builds the same first version. A cron job sums yesterday's spend, compares it to a limit and emails someone. It feels like a budget. It isn't. It runs after the money is spent, so the worst night of your year is the one it reports on and the one it could not have stopped. A runaway agent loop does not take a month to hurt you. It takes about ninety minutes.
A cap only means anything if it is checked before the call goes out. Ringside evaluates the Customer's remaining budget on the request path and returns 402 customer_budget_exceeded the moment the cap is reached. Your app gets an error code it can render as an upgrade prompt. Nobody gets paged at 3am, and there is no monitoring cron to keep alive.
Two shapes of limit
- • Monthly cap, for subscription products. Set
monthly_budget_usdwhen you create the Customer, or update it when they change plan. Calls past the cap return402 customer_budget_exceeded. - • Prepaid wallet, for credit-based and pay-as-you-go products. Top up with
POST /v1/customers/:id/wallet/topupand every call debits the balance. An empty balance returns402 customer_wallet_empty. A call that would cost more than the balance left returns402 customer_wallet_insufficientand runs nothing, rather than half-draining someone and failing anyway. - • Warnings before the wall. The
wallet.lowandcustomer.budget_exceededwebhooks fire on threshold crossings, so your billing service can auto-top-up, start dunning or open a sales conversation while the user is still working. - • Usage you can invoice from.
/v1/usagerolls up by customer, model and day. Pull it nightly and hand it to Stripe as metered quantity, which is the last block of the sample below.
What Ringside costs, plainly
Pooled credits carry a per-model markup, so the catalog rate is what your wallet is charged and that margin is how the platform earns. Worth saying out loud on a page about margin: your cost per token here is not the raw provider rate. What you get for the difference is the attribution, the enforced caps and the billing rollups, none of which you now have to build or run.
Architecture
In code
# On signup, create a Ringside Customer with a hard monthly cap.
customer = httpx.post(
"https://api.fightclub.pro/v1/customers",
headers={"Authorization": f"Bearer {RINGSIDE_KEY}"},
json={
"external_id": user.id,
"display_name": user.email,
"monthly_budget_usd": 25.00, # 402 customer_budget_exceeded once hit
},
).json()
# Every AI call carries the Customer header. That is the whole integration.
# client is the OpenAI SDK with base_url swapped to api.fightclub.pro/v1.
resp = client.chat.completions.create(
model="fc:openai/gpt-4o",
messages=[{"role": "user", "content": prompt}],
extra_headers={"FC-Customer": customer["id"]},
)
# Nightly: pull per-customer usage, push to Stripe as metered.
usage = httpx.get(
f"https://api.fightclub.pro/v1/customers/{customer['id']}/usage",
headers={"Authorization": f"Bearer {RINGSIDE_KEY}"},
params={"from": "2026-04-01", "to": "2026-04-30"},
).json()
stripe.UsageRecord.create(
subscription_item=user.stripe_item,
quantity=int(usage["total"]["llm_cost_usd"] * 100), # cents
timestamp=int(time.time()),
)