# Fix anonymous users in the dashboard (/docs/troubleshooting/anonymous-users)



Everything's arriving in the dashboard — but every call shows up as `anonymous` on the `free` plan,
so per-user and per-plan breakdowns are empty and plan-scoped rules never match the right people.
That means identity isn't reaching the wrapper. Those `anonymous` / `free` values are just the
defaults the SDK uses when nothing was attached.

## The usual cause: parameter names don't match [#the-usual-cause-parameter-names-dont-match]

Identity binding is **spelling-exact**. If the names are off, the values are silently ignored.

<Steps>
  <Step>
    **Python `@tp.workflow`** — the wrapped function's parameters must be named *literally*
    `user_id`, `paid_plan`, `session_id`. Not `userId`, not `uid`, not `plan`.

    ```python
    @tp.workflow(name="handle_turn")
    def handle_turn(user_id: str, paid_plan: str, session_id: str, message: str):
        return client.chat.completions.create(...)
    ```
  </Step>

  <Step>
    **Node `tp.workflow`** — the wrapped function's **first argument** must be an object carrying
    `userId` / `paidPlan` / `sessionId` (snake\_case keys are accepted too).

    ```typescript
    const handleTurn = tp.workflow(
      { name: "handle_turn" },
      ({ userId, paidPlan, sessionId, message }) => client.chat.completions.create({ ... }),
    );
    ```
  </Step>

  <Step>
    **Or set it at the call site** — wrap the call in a `session(...)` and pass the values directly,
    regardless of your function's parameter names:

    ```python
    with tp.session(
        user_id=user.id,
        paid_plan=str(user.plan or "free").strip().lower(),   # rules match the plan string exactly — normalize once, here
        session_id=convo_id,
    ):
        client.chat.completions.create(...)
    ```
  </Step>
</Steps>

<Callout type="warn">
  Normalize the tier value the same way everywhere you pass it. `paid_plan` is stored verbatim
  and matched byte for byte, so `"Free"` and `" free "` are different plans from `"free"` — the
  rule you meant to hit just never fires. See
  [Unexpected blocks](/docs/troubleshooting/unexpected-blocks).
</Callout>

See [Identity](/docs/concepts/identity) for the three fields and their defaults, and
[Cap free-tier users](/docs/recipes/cap-free-tier) for a worked `@tp.workflow` example.

## Related: conversations look single-turn [#related-conversations-look-single-turn]

If sessions aren't *grouping* — every turn shows as its own one-message conversation — the
`session_id` isn't **stable** across turns. A fresh id per call is a broken integration: re-use the
same id (your app's own thread/conversation id) for every turn of one conversation. The rules are in
the [stable-session\_id contract](/docs/concepts/identity#the-stable-session_id-contract).

<Callout type="info">
  These are correctness warnings, not blocks — TokenPolice keeps recording either way. Fixing
  identity just makes the dashboard's per-user, per-plan, and per-conversation views meaningful.
</Callout>

## Next [#next]

<Cards>
  <Card title="Identity" href="/docs/concepts/identity" description="The three fields, their defaults, and the session_id contract." />

  <Card title="Sessions & traces" href="/docs/concepts/sessions-and-traces" description="How turns thread into one conversation." />
</Cards>
