Skip to main content

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/knowledge sent 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.ai and X-Title: Brainstormer V2, identically on every environment, so dev, staging, production and a laptop were one app.

Where attribution may go

Attribution goes to provider APIs we hold an account with. It never goes to a site we scrape or to a customer-controlled endpoint.

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

OpenRouter documents 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 with appInfo: { 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 through httpOptions.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).
Never use NODE_ENV. It is the literal string production on dev, staging and production, so it discriminates nothing.
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 malformed WEB_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

  1. Import your service’s wrapper: import { openRouterHeaders, providerAttributionHeaders } from "../utils/attribution".
  2. Spread it into the request headers — ...openRouterHeaders() for OpenRouter, ...providerAttributionHeaders() for anything else.
  3. If the destination is a scrape target or a customer-controlled URL, send nothing and say why in a comment.
Each service carries a guard test (attribution-call-sites.test.ts) listing its call sites; removing a spread from one of them fails the build.