Per-customer usage dashboards with Client Tokens
25 minYour API key is the key to the building. You would never post it through a customer's letterbox, but that is what putting it in browser code does. A Client Token is a day pass instead: it opens one door, for one person, and it is void by lunchtime. By the end, every signed-in user can talk to a model straight from the browser on a day pass, and see a chart of their own spend that your server fetched with the real key.
- • A Ringside API key (completed the quickstart)
- • A Next.js app with working server-side auth (any provider)
- • A chart library (recharts works, or plain CSS bars)
- • Users already making Ringside calls tagged with FC-Customer
- • 25 minutes
Lazy-create an FC-Customer per user
// idempotent on external_id
Every signed-in user needs a 1:1 Ringside Customer so their spend is bucketed. Use the user's own id as external_id and look it up via ext:<id>.
On first login the lookup 404s, then you POST to create with display_name and any metadata you want back later. On every subsequent login the GET returns the existing row: no dupes, no race. Run this in your auth callback or on any server-side page load.
ext: lookups are Ringside's idempotency primitive. You never need to store the cus_... id in your own database - your user id is already the key. A customer belonging to another developer account reads as a 404, not a 403, so ids can't be probed.
Mint a Client Token
// short-lived, scoped, origin-locked
A Client Token is a signed token scoped to one customer and a fixed set of capabilities. TTL runs from 60 seconds to 1 hour and it cannot be refreshed. Mint a new one on page load, or shortly before the old one expires.
The mint body takes scope, origin_allowlist, ttl_seconds, rpm_limit and bind_ip. origin_allowlist binds the token to your domain; scope decides what it may call.
Valid scopes today are chat, conversations, embeddings, moderations and assistants_threads. Inference only. There is no browser-readable scope for customer or usage records, which is what Step 4 works around.
Expose /api/mint-client-token
// server-side gatekeeper
Browser code can't mint its own token; that would require exposing your Bearer key. Proxy through a Next.js route handler that first checks your own session, then calls Ringside with the server-side key.
Return the Ringside response straight through. Your server stays small; your Bearer key stays in your env vars. The browser gets token, expires_at and customer_id, and uses the token as Authorization: Client <token> on its chat calls.
Treat this route like a mint for physical day passes: if your own auth is compromised an attacker gets a 5-minute, origin-locked, rate-limited chat token for one customer. No broader blast radius. If a token expires mid-session the next call fails auth - re-call /api/mint-client-token, or rotate preemptively at expires_at minus 30s.
Read the spend on the server
// Bearer stays home
Spend is a billing record, not inference, so it is deliberately out of reach of a Client Token. Fetch it where the real key already lives: a server component or route handler calling GET /v1/customers/:id/usage?group_by=day with your Bearer key, then hand the numbers down as props.
The report returns a total object (llm_cost_usd, input_tokens, output_tokens, request_count) plus one buckets entry per day. Other group_by values: model, tag, property, conversation.
For lifetime figures and the budget you set, hit GET /v1/customers/:id instead: it returns lifetime_spend_usd, monthly_budget_usd and wallet_balance_usd.
This is the split that makes the whole design safe: the browser holds a capability that can only spend, never read the books, and the reading happens on a machine that already holds the key. Want the number to move live? Re-fetch it from your own route on an interval, or revalidate the server component. The Bearer key still never leaves your infra.
Chart the numbers
// recharts or CSS bars
The daily buckets are already chart-shaped. Feed buckets straight into a Recharts <LineChart> with dataKey="llm_cost_usd" against key on the X axis.
If you'd rather not add a chart lib, a grid of CSS flex bars with height: {spend / max * 100}% works fine and ships zero JS.
You built it.
Every signed-in user talks to a model from the browser on a token that expires in minutes, and sees a chart of their own spend that your server fetched with the real key. The Bearer key never left your infra, and the thing the browser holds cannot read anyone's books, including its own owner's.