Overview
Every request Brainstormer makes to a provider we hold an account with carries the environment, the calling service and the host that made it. The provider’s own dashboard can then split spend by environment instead of reporting one undifferentiated total. Before this, OpenRouter reported 148M of 150M tokens in a 24-hour window against an app named “Unknown”. Two causes:services/knowledgesent no attribution at all, and it owns graph extraction, visual entity extraction and document summarisation — most of the platform’s token volume.- The services that did send headers hardcoded
HTTP-Referer: https://brainstormer.aiandX-Title: Brainstormer V2, identically on every environment, so dev, staging, production and a laptop were one app.
Where attribution may go
What is sent
One helper builds every value:packages/shared/src/attribution.ts (pure — no
IO, no node:*), wrapped per service by src/utils/attribution.ts, which reads
os.hostname() and the environment.
Every paid provider
Brainstormer/<version> (<environment>; <service>; <host>).
OpenRouter, additionally
HTTP-Referer as an app’s identity (“your app’s URL
becomes its unique identifier”) and X-Title as its display label, which
cannot create an app on its own. The referer is built from WEB_APP_URL, which
already differs per environment, with a /svc/<service> path so the identity
agrees one-to-one with the title.
OpenRouter’s docs do not say whether a path is part of the app key or whether
the referer is normalized to its origin. If paths key separately, the
dashboard shows one row per environment and service. If not, several
titles collapse onto one origin row — which is what a path-less referer would
have produced anyway.
Stripe, additionally
Stripe has first-class attribution, so it gets that instead of only a User-Agent: the SDK is constructed withappInfo: { name, version, url },
which Stripe surfaces in its request logs.
Gemini
Gemini offers no first-class app attribution — its console splits by project and credential, not by request — so the embedding client sends the User-Agent throughhttpOptions.headers. The SDK’s x-goog-api-client header, which
Google parses for client telemetry, is deliberately left untouched.
Where the environment comes from
SENTRY_ENVIRONMENT. It is already set per environment and differs correctly
(dev-server on the dev box; docker-compose.yml defaults it to production).
An unset SENTRY_ENVIRONMENT resolves to local. Compose supplies a value to
every deployed container, so “unset” means the process is not running under our
compose files — a developer’s laptop or an offline eval, which is exactly what
local should mean.
Offline eval harnesses in services/knowledge/eval/ report their own service
names (knowledge-eval, embedding-eval), so an eval run is never counted as
production traffic.
Failure behaviour
The helper never throws. An unreadable hostname, a missing environment, a malformedWEB_APP_URL and an empty service name each degrade to a documented
default — no attribution is far better than a failed provider call. Every
segment is reduced to printable ASCII and bounded at 64 characters, so a value
cannot break a header.
Privacy
os.hostname() is the container id in Docker, which is what distinguishes two
replicas and a laptop from a deployed box. On a customer-hosted deployment a
hostname can be mildly identifying. It carries no secret and is sent only to the
paid providers above, never to a customer endpoint or a scrape target.
Adding a call site
- Import your service’s wrapper:
import { openRouterHeaders, providerAttributionHeaders } from "../utils/attribution". - Spread it into the request headers —
...openRouterHeaders()for OpenRouter,...providerAttributionHeaders()for anything else. - If the destination is a scrape target or a customer-controlled URL, send nothing and say why in a comment.
attribution-call-sites.test.ts) listing its
call sites; removing a spread from one of them fails the build.
