If you sell to other developers shipping an AI product (Auth0 of LLMs, multi-tenant SaaS, B2B copilots), the question your customers ask first is:Documentation Index
Fetch the complete documentation index at: https://docs.voight.xyz/llms.txt
Use this file to discover all available pages before exploring further.
“What’s the cost per end-user this month?”Per-user spend is the answer. One line of code at your request boundary tags every LLM call with the user it belongs to. The dashboard’s Users sub-tab then aggregates spend, traces, tokens, and tool use per user — without you handing Voight any PII you don’t already own.
The 1-line pattern
Wrap your provider client once (the OpenAI or Anthropic SDK), then at your request boundary callwithTrace and pass a tags.userId:
metadata.tags = { userId, plan, org } on every event inside the withTrace block via AsyncLocalStorage — no manual passing through call sites, no middleware to wire.
Why this design
We considered three alternatives and rejected them:| Pattern | Why we didn’t | Why explicit tagging won |
|---|---|---|
| Auto-detect user from request headers | Fragile (every auth system different), and means Voight reads your auth context | You stay in control of what’s tagged |
| Require a “Voight user object” via dashboard | Forces a sync flow, breaks for SSO/SCIM apps | Zero coupling to user-management |
| Capture client IP / fingerprint | GDPR landmine, useless for B2B SaaS | Tags are your IDs, not ours |
userId is — a database ID, an Auth0 sub, a Clerk user.id, a hashed email, anything. Voight stores it as a tag string. We never see the underlying user record.
Examples for common auth systems
Auth0 (Express middleware)
Auth0 (Express middleware)
Clerk (Next.js Route Handler)
Clerk (Next.js Route Handler)
NextAuth.js / Auth.js (App Router)
NextAuth.js / Auth.js (App Router)
Custom JWT (any framework)
Custom JWT (any framework)
Anonymous / unauthenticated paths
Anonymous / unauthenticated paths
What you see in the dashboard
Open the Users sub-tab under AI Apps. You get one row per uniqueuserId value, sorted by spend (descending by default):
| Column | Aggregated from |
|---|---|
| User | metadata.tags.userId — your tag value, shown verbatim |
| Spend (USD) | Sum of cost per event for that user in the time window |
| Traces | Number of withTrace blocks that user triggered |
| Tokens | Input + output + cache reads + cache creations |
| Last seen | MAX(timestamp) for any event tagged with that user |
| Top model | The model that user spent the most on |
withTrace block they triggered), then into individual events (every LLM call, with prompt, response, tokens, latency).
The User filter pill in the top bar lets you narrow the entire AI Apps section (Overview, Traces, Models, Tools) to one user — same way the Provider and Agent pills work.
Empty state — why don’t I see any users?
If you’ve wired the wrapper but the Users tab shows zero users, it’s almost always one of these:- You’re not calling
withTrace— the wrapper captures everything, buttagsonly flow into events that ran inside awithTraceblock. Calls made outside it have no user attribution. Wrap your request boundary. - You’re not passing
tags.userId—withTraceaccepts an optionaltagsmap. WithoutuserId, the event has no user to attribute to. - Time window — the Users tab respects the dashboard’s window selector (default 28 days). Switch to 7d or 1d if you only deployed today.
- Privacy: minimal — Minimal mode strips prompts and responses but keeps tokens, costs, and tags. Users tab works in Minimal. If you set up something stricter (custom redaction layer), confirm your tags aren’t being filtered out.
tags.userId field on withTrace). After your next request the user appears in the dashboard within ~2 seconds.
Multiple tags, multiple dimensions
tags is a flat string-to-string map. You can attribute by any dimension you care about:
userId as the primary user dimension (drives the Users sub-tab). The other tags are queryable via the API — GET /v1/me/ai-apps/overview?tag.plan=pro returns metrics scoped to Pro users only. Future dashboard releases will surface tag-based segmentation natively (cost-per-plan, cost-per-feature).
FAQ
What about GDPR?
What about GDPR?
Tag values are stored verbatim as strings. Choose what to send accordingly.
- ✅ Database IDs, Auth0
sub, Clerk IDs — these are pseudonymous identifiers, not personal data on their own - ✅ Hashed emails (SHA-256), opaque session IDs
- ⚠️ Raw email addresses, names, IPs — personal data; only send if your DPA covers it
- ⚠️ Anonymous user fingerprints — may be personal data depending on jurisdiction
Does this work in multi-tenant SaaS?
Does this work in multi-tenant SaaS?
Yes — that’s the primary use case. Tag every request with
userId (the end-user inside your customer’s tenant) and org (the customer tenant). Then:- Filter the dashboard by
org=acme-corpto see one tenant’s traffic - Drill into individual users inside that tenant
- Export per-tenant cost reports via
GET /v1/me/ai-apps/users?tag.org=acme-corp
What if I don't have auth?
What if I don't have auth?
If your app is public (a marketing site assistant, a free demo), still call This gives you abuse signals (one anon hammering 1000 calls/hour) and per-session cost visibility without auth.
withTrace — just use a stable browser-scoped identifier for userId:Does adding tags slow down my app?
Does adding tags slow down my app?
Can I tag from the client side?
Can I tag from the client side?
No —
withTrace runs server-side (where the wrapped OpenAI / Anthropic client lives). The tag value is set once at the request boundary in your backend and propagates to every LLM call that handler triggers.If you have a serverless setup (Next.js Route Handlers, Vercel Functions, AWS Lambda), withTrace still works — AsyncLocalStorage is part of Node’s runtime, not tied to long-lived processes.What if my customers want their own dashboard?
What if my customers want their own dashboard?
Two paths:
- You operate one Voight account, tag by
org, and expose internal per-tenant views to your customers via your own admin UI (queryable via/v1/me/ai-apps/*). - Each customer has their own Voight account with their own API key. You run two wrappers in parallel (yours for ops; theirs for their customer-facing dashboard).
Wiring it end-to-end
If you haven’t installed the wrapper yet, the order is:- Install the wrapper for your provider — OpenAI or Anthropic
- Wrap your client at module load time (one place in your codebase)
- Call
withTraceat your request boundary withtags.userId - Open the dashboard → AI Apps → Users sub-tab
- Profit (literally — you now know which users to upsell, throttle, or upgrade)
Next
- AI Apps overview — the dashboard section that surfaces per-user data
- Tracing — the
withTrace+logAPI in full - OpenAI SDK — wrap your OpenAI client
- Anthropic SDK — wrap your Anthropic client