
What it does
Send a stable identifier for your own customer on every request and each call's settled cost is recorded against that customer. At period end you have a cost per account that sums exactly to your gateway bill — the input to a usage-based invoice line, a margin number, and a per-customer hard cap.
The one-paragraph version: if you sell an AI product, your cost of goods sold is a function you cannot currently evaluate. You have one provider total and a customer table, and no join between them. The join is a single field on the request. Once it is there, "what did Acme cost us in July" stops being an estimate and becomes a filter, and everything downstream of that number — pricing, margin, quota, the decision to fire a customer — becomes decidable.
This post is about attributing and re-billing spend to your customers. It is deliberately not about capping your own total (budget ceilings), not about spending less per call (routing as a cost lever), and not about reaching many providers at all (one key, every provider). It assumes those and builds the reseller layer on top.
The job it does for you
Before. The month closes. You have one number from the gateway, one number from your Stripe dashboard, and a spreadsheet in between built out of proxies — seats, requests, a per-customer average someone computed in March. The spreadsheet is wrong in a way you cannot bound, which produces three specific failures:
- You cannot offer usage-based pricing, because you would be billing on an estimate that can be wrong in either direction. Wrong high is a refund and a support ticket; wrong low is silent margin loss.
- You cannot find the unprofitable account. Flat-plan customers do not distribute evenly; a small number of them consume most of the model spend, and without attribution they are invisible until the aggregate hurts.
- You cannot contain one customer's runaway. A retry loop in one tenant's integration consumes the same budget that serves everyone else, and the first signal is the whole product degrading.
After. Cost per customer is a query. Margin per customer is a join against your billing system. A usage-based line item is a report, not a metering project. And a noisy tenant hits their own ceiling rather than yours.
The important part is the last one. Attribution and containment are usually sold as one feature and they are two: measuring what a customer cost you is bookkeeping, while stopping a customer from costing you more is enforcement. You want both, and you configure them in different places.
How it works
Every chat, completion and embedding request may carry a top-level user field — the same field the OpenAI request schema describes as "a stable identifier for your end-users". Whatever string you send is recorded verbatim and aggregated by exact match. It is treated as an opaque identifier; nothing is joined against it on our side.
One note on provenance, because it changes what the field means depending on who is reading it. OpenAI is in the process of splitting user into two narrower parameters of its own — safety_identifier for abuse detection and prompt_cache_key for cache bucketing, both described in OpenAI's safety best practices. That is a change to what OpenAI does with the value, not to the wire shape: the top-level user field is still accepted, and it is the field read for attribution here. Anthropic's own analogue is metadata.user_id on the Messages API, which carries the same instruction in stronger words — "this should be a uuid, hash value, or other opaque identifier".
That gives you a fifth level of separation underneath four you already have:
Organization
└─ Team — budgets and RPM/TPM per team
└─ Virtual key — budgets, RPM/TPM, model allowlist per key
└─ user — your customer, per requestChoosing which level a customer lives on is the whole design decision, and it turns on one question: do you need to stop this customer, or only to count them?
| Shape | What you send | Choose it when |
|---|---|---|
Shared key, user per customer | One key for the app, user: "tenant_acme" on each call | Many customers, you need the breakdown and per-customer overrides |
| Key per customer | A dedicated virtual key per tenant | You need revocation, a model allowlist, or a blast radius of exactly one account |
| Team per customer | A team owning that customer's keys | The customer maps to an internal unit that also owns budget |
Revocation is the tiebreaker people miss: cutting off a customer means revoking a key, so if you need to be able to shut one customer off, that customer needs their own key. Counting alone does not.
The identifier shape matters too, and it is worth deciding once rather than discovering later that half your rows are unmergeable:
| Pattern | Example | Granularity you get |
|---|---|---|
| Tenant / workspace | tenant_acme | One row per customer account |
| End-user / seat | u_8f3a1b9c | One row per person |
| Product line | proj_assistant | One row per feature you sell |
| Composite | tenant_acme:u_8f3a1b9c | Filter at either level |
Send an opaque, stable identifier from your own database — a UUID or a hash. Do not send an email address, a name or a phone number; both providers cited above say so explicitly, Anthropic's reference in as many words ("do not include any identifying information such as name, email address, or phone number"). The value flows onward into provider logs, and putting personal data there turns a future data-subject request into an excavation. Redacting PII from LLM logs covers the wider version of that discipline.
Set it up
One line in the call site, and it is a first-class SDK argument — not extra_body, not a message, not a header.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.nrouter.ai/v1",
api_key=os.environ["NROUTER_API_KEY"],
)
def answer(prompt: str, tenant_id: str):
return client.chat.completions.create(
model="claude-sonnet-4-5-20250929",
messages=[{"role": "user", "content": prompt}],
user=tenant_id, # ← the only line you add
)// TypeScript — same field, same level.
const completion = await client.chat.completions.create({
model: 'claude-sonnet-4-5-20250929',
messages: [{ role: 'user', content: prompt }],
user: tenantId,
});Raw HTTP is the same field at the top level of the body:
curl -s https://api.nrouter.ai/v1/chat/completions \
-H "Authorization: Bearer $NROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5-20250929",
"messages": [{"role": "user", "content": "hello"}],
"user": "tenant_acme"
}'Two operational notes that save a week. First, add it everywhere at once. A call without the field is recorded for billing but attributed to nobody, so a partial rollout produces a bucket of unattributed spend that looks exactly like a reporting bug. Second, the string must match character for character across your codebase — tenant_acme and tenant-acme are two customers. Normalise at one function boundary, not at each call site.
The full walkthrough, including how to verify the rows are landing, is attribute LLM spend by team, customer, and feature, and the dashboard surface it populates is documented at API consumers.
Worked example with numbers
A three-tenant month, with figures you should replace with your own. The costs come from summing x-nr-request-cost over the period; the revenue comes from your billing system.
| Customer | Plan revenue | Model cost | Gross margin | Margin % |
|---|---|---|---|---|
| Acme | $499 | $61.40 | $437.60 | 88% |
| Borealis | $499 | $412.05 | $86.95 | 17% |
| Cedar | $99 | $147.80 | −$48.80 | −49% |
| Total | $1,097 | $621.25 | $475.75 | 43% |
Illustrative figures. The method is the point, not the numbers.
The aggregate says 43% and looks fine. The rows say something else entirely: one customer on a $99 plan is losing you money every month, and one customer on the same $499 plan as Acme costs nearly seven times as much to serve. Neither fact is visible in a monthly total, and both are actionable the moment they are visible — Cedar moves to usage-based or to a capped plan, Borealis is the account you check before offering that plan tier again.
Then reconcile, which is the step that makes the whole thing trustworthy:
1. sum cost per customer over the period
2. add the unattributed bucket (calls with no user field)
3. compare against the credit ledger for the same window
4. the difference must be the unattributed bucket, and nothing elseBecause the per-call figures are settled costs rather than estimates, step 4 closes exactly. If it does not, you have found either a gap in your rollout or a genuine defect — and both are worth knowing before you invoice on the number. How to read your LLM credit ledger is the other half of that reconciliation.
Turning attribution into an invoice line
The billing model you can now offer, in increasing order of how much of the risk you keep. All four are ordinary shapes in any billing system — the bottom two are what a metering integration such as Stripe's usage-based billing expects you to feed it, and the number you feed it is the one this post is about producing:
| Model | How you compute it | Who carries the variance |
|---|---|---|
| Flat plan, informed by cost | Set the price above the p95 of measured cost per customer | You |
| Flat plan with a cap | Flat price, hard ceiling per customer at the plan's included spend | Shared |
| Usage-based with markup | Measured cost × your multiplier | Your customer |
| Cost pass-through plus fee | Measured cost + a platform fee of your own | Your customer |
Two cautions before you pick the bottom two. A markup on a measured number is only honest if the number is the settled cost, which it is — but it means your customer's bill moves when a provider changes a rate, so say that in the contract. And an unpriced call is reported as unpriced rather than as zero; the cost header is absent in that case, paired with x-nr-cost-status: unpriced. Your billing code must handle "cost unknown" as its own state instead of coercing it to 0, or you will silently invoice nothing for real work. The reasoning behind that choice is cost honesty, and it is a design constraint on your metering, not a footnote.
Capping a customer, not just counting them
Measurement does not stop a runaway. For that you need a ceiling on the customer, and there are two places to put one.
On the customer's own key. A virtual key carries a budget, an RPM limit and a TPM limit. When a per-key budget is exhausted the request is refused with 429 and the code key_budget_exceeded — note that this differs from the org, team and user scopes, which refuse with 402 and budget_exceeded. Reading the code rather than the status is what lets your client tell "this customer is out of allowance" apart from "the whole account is out of credit", and handling 429 and 402 is the client-side implementation.
On the consumer row directly. You can attach RPM, TPM and budget overrides to an individual consumer, including before their first request ever lands — useful when you onboard a tenant onto a quota plan and want the cap live from call one rather than from the first reconciliation. The API and the dashboard flow are both in API consumers.
The general rule for which control to reach for is in budgets vs rate limits: a budget bounds the money, a rate limit bounds the speed, and a customer whose integration retries in a tight loop needs the second one even though the first is what you were worried about.
What it costs
Nothing extra. Attribution, per-customer caps, budgets and rate limits are on every plan, and the fee that does vary between plans is charged on top, at purchase — 4% of your credits on pay as you go, 0% on Pro — never folded into the per-call rate, which is precisely what lets a month of calls be split across customers and still sum back to the invoice; the plan table is on the pricing page.
For a reseller the fee is a direct COGS line, so it belongs inside the margin table above rather than beside it. Because the fee is a flat 4% of the credits, it comes to 4% of your provider spend: on the illustrative month, $621.25 of model cost carries a $24.85 fee, which takes the aggregate margin from $475.75 to $450.90 (41%) — and Cedar's $5.91 share of it deepens that account's loss from $48.80 to $54.71. On Pro the fee is zero and the $50 subscription is a fixed line instead; the crossover is $1,250/month of provider spend on the monthly plan and $1,042/month on annual, worked through in from credits to Pro.
There is no free tier under any of this: the card comes first and the $5 minimum is a real charge with the platform fee on top.
Where it fits with the rest of the platform
- Scoping. Customers map onto an org/team/member hierarchy that already exists; org, team, member is how keys, budgets and guardrails hang off it.
- Key design. One key per customer is a blast-radius decision before it is a billing one — virtual keys vs master key.
- Agent workloads. If your product is agentic, a single customer action fans out into many calls, and attributing the fan-out is its own problem: multi-agent cost tracking.
- Finding the expensive thing. Once spend is sliced by customer, slice it by model too — cost vs usage is how the quietly expensive model surfaces.
- Logging. What you keep about a customer's calls is a compliance decision as much as a debugging one: what to log and not log.
Limits and what it will not do
- It does not invoice your customers. It produces the number; your billing system produces the invoice. There is no Stripe integration that bills your end customers on your behalf.
- The identifier is opaque and unvalidated. Nothing checks that
tenant_acmeis a real customer of yours. A typo creates a new row rather than an error, so normalise centrally. - Calls without the field are unattributed, not retroactively fixable. You can start attributing tomorrow; you cannot attribute last quarter. That is the strongest argument for adding the line before you need the report.
- Attribution is not authorization. The
uservalue is metadata for your books. It never decides whose budget is charged — that comes from the authenticated key. Do not build access control on it. - There is no "block this user" switch. Revocation lives on the key. If you need to be able to cut a customer off, give them their own key.
- An unpriced call is unpriced. The cost header is absent, not zero. Metering code that assumes a number is always present will under-bill silently.
- SOC 2 Type II is in progress, not certified. The trust page carries the current posture.
Try it
Create an account at signup, load the $5 minimum (platform fee on top), and add user=<your tenant id> to one call site. Send a single real request, then open the consumers view and confirm the row appears with a cost on it. That round trip takes about ten minutes with the quick start and it is the whole proof — everything else in this post is arithmetic on top of that one row.
See also
- Attribute LLM spend by team, customer, and feature — the step-by-step rollout, including how to verify the rows are landing.
- How to read your LLM credit ledger — the reconciliation that makes a usage-based invoice safe to send.
- Handling 429 and 402 errors from an LLM gateway — telling a customer's exhausted cap apart from your account running dry.
- Virtual keys vs master key: scoping a key per job — when a customer needs their own key rather than a shared one.
- Multi-agent cost tracking — attributing a fan-out when one customer action becomes forty calls.
- Cost honesty: we read the number, we do not invent it — why your metering code must handle unpriced as a state.
- Pricing — the platform fee that belongs inside your COGS line, in its live form.
Sources
Verified 2026-08-23. If any linked page has changed since, email hello@nrouter.ai and we will correct this post.
- nRouter plans and fees: nrouter.ai/pricing — $0 subscription with a platform fee of 4% of your credits, Pro at $50/mo or $500/yr at 0%, $5 minimum purchase.
- nRouter API consumers documentation: nrouter.ai/docs/guides/end-users — the
userfield, consumer rows, and per-consumer overrides. - OpenAI API reference —
userfield: platform.openai.com/docs/api-reference/chat — the request schema this field follows. - OpenAI safety best practices — safety identifiers: platform.openai.com/docs/guides/safety-best-practices — the
safety_identifierandprompt_cache_keysplit, and the recommendation to hash a username or email rather than send it. - Anthropic Messages API —
metadata.user_id: platform.claude.com/docs/en/api/messages — the provider-native analogue, and the source of the "opaque identifier / no identifying information" wording quoted above. - Stripe usage-based billing: docs.stripe.com/billing/subscriptions/usage-based — what a metered invoice line consumes, for the two pass-through models in the billing table.
- Anthropic pricing: anthropic.com/pricing — published per-model rates behind the illustrative cost figures.
- AWS Bedrock pricing: aws.amazon.com/bedrock/pricing — Bedrock is live on nRouter and AWS publishes its own rates.
OpenAI, Anthropic, AWS and Stripe are trademarks of their respective owners. nRouter is not affiliated with or endorsed by them. All claims above are sourced from their public pricing or documentation on the date shown.


