> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firedog.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Attribution

> How every AI call gets filed to a desk, workflow and user — the three context fields, the headers they travel on, and how each maps to a dashboard lens.

Attribution is what turns one opaque AI invoice into a bill you can defend. Every call routed through the collector carries three optional context fields — `team`, `workflow`, `userId`. The collector stamps them onto the call metadata it emits to the control plane, and each field drives one lens in the dashboard.

You set attribution once as a default and refine it per call. Nothing about attribution changes what leaves your VPC: the full prompt and response stay in your [local Postgres](/how-it-works), and only the tags below — hashed according to your [sensitivity profile](/sensitivity-profiles) — reach Firedog cloud.

## The three context fields

<ParamField path="team" type="string">
  The desk or cost centre that owns the spend — for example `macro-desk`, `credit-research`, `quant-pnl`. Rolls up into the **desk** lens in [Cost allocation](/how-it-works), so finance can attribute and charge back a run to the group that spent it. Usually set once on `defaultContext` per application or service.
</ParamField>

<ParamField path="workflow" type="string">
  The named process the call belongs to — for example `market-research`, `kyc-review`, `earnings-summary`. Populates the **workflow register** on Home (the reconciled bill, per workflow) and opens the step-by-step [workflow detail](/how-it-works) view where the re-read tax is visible. Best set per call, since one service often runs several workflows.
</ParamField>

<ParamField path="userId" type="string">
  The end user or service account behind the call — for example `u_123`. It is **always salted-hashed inside your VPC** before any metadata leaves it, so the dashboard shows stable per-user cost buckets (`user_9f3c…`) without ever receiving a raw identity. Pass your own internal identifier; the collector does the hashing.
</ParamField>

<Note>
  All three fields are optional. A call with none of them still prices, still writes its full record locally, and still reconciles into the total — it just files under **Untagged**. See [Untagged still reconciles](#untagged-still-reconciles) below.
</Note>

### Field-to-lens map

| Field      | Header               | Dashboard lens                            | How it leaves the VPC                              |
| ---------- | -------------------- | ----------------------------------------- | -------------------------------------------------- |
| `team`     | `x-firedog-team`     | Desk (Cost allocation)                    | Raw under `standard`/`open`; hashed under `strict` |
| `workflow` | `x-firedog-workflow` | Workflow register (Home, Workflow detail) | Raw under `standard`/`open`; hashed under `strict` |
| `userId`   | `x-firedog-user`     | Salted per-user                           | Always salted-hashed, every profile                |

## Setting attribution

There are two ways to attach context, and they produce identical metadata. Use the SDK if your code calls the SDK; set the headers directly if you route through the collector as a drop-in proxy.

### With the SDK

`defaultContext` applies to every call from a client. Per-call `context` is merged on top, so shared tags live on the client and the variable tag rides with the request.

```ts theme={null}
import { createClient } from "@firedog/sdk";

const firedog = createClient({
  collectorUrl: "https://collector.internal",   // your in-VPC collector
  apiKey: process.env.PROVIDER_KEY,              // forwarded to the upstream provider
  defaultContext: { team: "macro-desk" },        // applied to every call
});

const res = await firedog.chat.completions.create({
  model: "opus",
  messages: [{ role: "user", content: "Summarise today's rates moves." }],
  context: { workflow: "market-research", userId: "u_123" }, // merged per call
});

// This call files under team=macro-desk, workflow=market-research,
// and a salted hash of u_123.
console.log(res.callId);
```

<Tip>
  Keep `team` on `defaultContext` and set `workflow` (and `userId` where you have one) per call. That mirrors reality: a service belongs to one desk but runs many workflows.
</Tip>

### With raw headers (proxy path)

If you point an existing OpenAI-compatible client at the collector's base URL instead of using the SDK, set the same attribution as `x-firedog-*` headers. You also mint the correlating `x-firedog-call-id` yourself — a client-side ULID that ties this request to its full VPC record. (The SDK does both for you.)

```bash theme={null}
curl https://collector.internal/v1/chat/completions \
  -H "Authorization: Bearer $PROVIDER_KEY" \
  -H "Content-Type: application/json" \
  -H "x-firedog-call-id: 01J8ZG9M0P4QW7K2YB3H6RS8TN" \
  -H "x-firedog-team: macro-desk" \
  -H "x-firedog-workflow: market-research" \
  -H "x-firedog-user: u_123" \
  -d '{
    "model": "opus",
    "messages": [{ "role": "user", "content": "Summarise today'\''s rates moves." }]
  }'
```

<Info>
  The header names are the wire format for both paths — the SDK simply serialises `context` into `x-firedog-team`, `x-firedog-workflow` and `x-firedog-user`, and mints `x-firedog-call-id`.
</Info>

## How context becomes metadata

The collector prices the call, writes the full record (messages, any RAG chunks, response) to your local Postgres, and emits only `CallMetadata` to the control plane. The attribution fields on that metadata are exactly your three tags — with `userId` already hashed, and `team`/`workflow` hashed or raw depending on the profile:

```json theme={null}
{
  "callId": "01J8ZG9M0P4QW7K2YB3H6RS8TN",
  "team": "macro-desk",
  "workflow": "market-research",
  "userHash": "user_9f3c1e7a",
  "model": "opus",
  "promptTokens": 1840,
  "completionTokens": 320,
  "cost": 0.0516
}
```

<Warning>
  There is no raw `userId` field in the metadata — the collector emits `userHash` and nothing else for user attribution. The salt lives in your VPC, so the same person maps to a stable bucket in the dashboard, but the cloud never holds a value you could reverse to an identity.
</Warning>

## Hashing and sensitivity

What gets hashed before it leaves the VPC is governed by your [sensitivity profile](/sensitivity-profiles), a configurable default rather than a hard guarantee:

<CardGroup cols={3}>
  <Card title="strict" icon="lock">
    Maximal redaction. `team` and `workflow` are hashed to opaque tokens (for example `Project-Titan` → `wf_7c2a`); `userId` is hashed; identifying categories may be dropped.
  </Card>

  <Card title="standard" icon="scale-balanced">
    Balanced. Raw `team` and `workflow` labels, salted `userHash`, quality signal retained.
  </Card>

  <Card title="open" icon="unlock">
    Richest tags, for teams whose labels are not sensitive.
  </Card>
</CardGroup>

Regardless of profile, `userId` is **always salted-hashed** — there is no setting that emits a raw user identity. Under `strict`, a desk whose name is itself sensitive (a codenamed project, a client mandate) still reconciles correctly in the dashboard; it just appears under a stable hash instead of a readable label.

## Untagged still reconciles

Attribution is additive, never a gate. A call with no `team`, no `workflow` and no `userId` is still priced, still written in full to your VPC, and still counted in the total — it rolls up under **Untagged**. On Home, tagged plus untagged spend equals the reconciled bill to the cent, so nothing is dropped for lacking a label and the "Untagged" line tells you exactly how much attribution work is left to do.

<Check>
  A healthy rollout drives the Untagged line toward zero over time by adding `defaultContext` to more services.
</Check>

## Next steps

<CardGroup cols={2}>
  <Card title="Sensitivity profiles" icon="shield-halved" href="/sensitivity-profiles">
    Decide what the collector is allowed to emit for each tag.
  </Card>

  <Card title="Quality & shadow testing" icon="clipboard-check" href="/quality-shadow-testing">
    Score answers and prove a cheaper model holds up — without leaking content.
  </Card>

  <Card title="Security & data residency" icon="lock" href="/security">
    What stays in your VPC, and what the metadata contains.
  </Card>
</CardGroup>
