# Identity: user_id, paid_plan, session_id (/docs/concepts/identity)



Rules don't act on your whole app at once — they target *slices* of traffic. The tags below
define those slices. Attach them and a rule can say "cap free-plan users", "stop this one
runaway conversation", or "watch what the summarizer step costs".

None of them are required. Skip them all and TokenPolice still meters everything; you just
can't tell one user, plan, or conversation from another.

## The fields [#the-fields]

| Field         | What it is                                                             | If you don't set it                                                       |
| ------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `user_id`     | Which end user made the call                                           | `"anonymous"` — all spend lands in one pool, so only app-wide limits work |
| `paid_plan`   | Which pricing tier that user is on                                     | `"free"`                                                                  |
| `session_id`  | Which conversation or run this call belongs to                         | a fresh id per run, so nothing threads together                           |
| workflow name | What this run *is* — `"support_agent"`, `"nightly_report"`             | `"default_workflow"`                                                      |
| span name     | What this one LLM step is — `"summarize"`, `"route"`                   | the model's name                                                          |
| `metadata`    | Any other tags you want to slice by — `tenant_id`, `feature`, `region` | empty                                                                     |

`user_id` and `paid_plan` are the two that make per-user and per-tier budgets possible.
`session_id` is what turns a multi-turn chat into one costed conversation. The names are what
make the dashboard readable — and all of them are things a rule can match on.

<Callout type="warn">
  **`paid_plan` is compared exactly.** It isn't trimmed and isn't lowercased, so `"Free"`,
  `" free "` and `"free"` are three different plans: a rule looking for `free` won't match the
  first two, and a rule that pools by plan gives each spelling its own budget. Normalize once,
  in your app — `str(tier or "free").strip().lower()` / `String(tier ?? "free").trim().toLowerCase()`
  — and use that same spelling when you write the rule.
</Callout>

## The stable-session\_id contract [#the-stable-session_id-contract]

`session_id` is the one field you have to be deliberate about: &#x2A;*pass the same id on every
turn of the same conversation.** A new id per turn isn't a smaller mistake — it means the
conversation never exists as far as TokenPolice is concerned, and per-conversation caps can
never fire.

Use whatever your app already has: a chat id, a thread id, a support ticket number. What the
SDK does with it:

* Trims it and strips control characters.
* Caps it at **200 characters**.
* If it's empty or missing, falls back to a fresh id for that run alone.

If your app really is one-shot per request, leaving it out is a fair choice — just know that
conversation grouping and per-conversation limits won't apply.

## Setting them [#setting-them]

The usual way is to wrap your entrypoint and let it read the values off the call.

```python
import token_police as tp

@tp.workflow(name="support_agent")
def handle(user_id: str, paid_plan: str, session_id: str, query: str):
    ...   # every LLM call inside carries these three
```

```typescript
import * as tp from "token-police";

const handle = tp.workflow({ name: "support_agent" }, async (req: {
  userId: string; paidPlan: string; sessionId: string; query: string;
}) => {
  // every LLM call inside carries these three
});
```

Python reads arguments **by name** — a parameter literally called `user_id`, `paid_plan`,
`session_id` or `metadata`. Node reads the same keys off the **first argument when it's an
object** (`userId`, `paidPlan`, `sessionId`; snake\_case works too). Either way you can also
pass the values statically, or open a `session(...)` block instead of wrapping a function —
the full API is in [Wrapping your app](/docs/sdk/wrapping).

To name one step inside a run, call [`set_span_name`](/docs/sdk/set-span-name) just before it.

<Callout type="info">
  Nested scopes inherit the enclosing scope's ids and merge its metadata, so you set identity
  once at the top and everything below is tagged.
</Callout>

## Next [#next]

<Cards>
  <Card title="Wrapping your app" href="/docs/sdk/wrapping" description="workflow, session, agent, chain — the exact APIs." />

  <Card title="Sessions, workflows & traces" href="/docs/concepts/sessions-and-traces" description="How these tags shape the trace of a run." />

  <Card title="Anonymous users" href="/docs/troubleshooting/anonymous-users" description="Why everything shows as anonymous, and how to fix it." />
</Cards>
