# Do it yourself, the way the skill does it (/docs/get-started/integration-checklist)



The [coding-agent skill](/docs/get-started/coding-agent/overview) follows a fixed procedure.
This page is that procedure, written for a person. Work through it in order and you end up
with the same integration the skill would have written.

It's language-neutral on purpose. The code for each step is in
[Manual setup — Python](/docs/get-started/manual-python) and
[Manual setup — Node](/docs/get-started/manual-node).

## Decide these five things first [#decide-these-five-things-first]

The skill asks its questions in one batch before it edits anything, because every one of them
changes the code it writes. Answer them now and the rest is mechanical.

| Question                                                     | Why it matters                                                                                                                                                        | If you have no answer                                                     |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Which value identifies a user?**                           | Per-user budgets key off it. Names differ by app — `uid`, `sub`, `account_id`, `customer.id`.                                                                         | Everything pools under `anonymous` and only app-wide limits work.         |
| **Which field holds the plan or tier?**                      | Lets one rule treat free and paid users differently.                                                                                                                  | Everything is recorded as `free`. Pick `"default"` now and swap it later. |
| **Which id is stable across the turns of one conversation?** | It's what groups a multi-turn chat into one costed thread. A thread id, chat id, ticket number.                                                                       | Each run stands alone; per-conversation limits can't fire.                |
| **Dry-run or enforce?**                                      | Whether a rule can actually stop a call. Start in **dry-run**.                                                                                                        | Dry-run is the default, and it's the right default.                       |
| **What should a blocked call return?**                       | HTTP apps need to turn the SDK's error into a status code. **429 Too Many Requests** is the sensible default; **402 Payment Required** fits a billing or upgrade cap. | Only matters once you're on enforce — but decide before you flip it.      |

<Callout type="info">
  Don't rename anything in your app to match TokenPolice. Map whatever you already have onto
  these fields at the point where you wrap. If a value exists but isn't visible where you want
  to wrap, pass it through as a named argument — the SDK picks it up from there.
</Callout>

## The checklist [#the-checklist]

<Steps>
  <Step>
    ### Install it — and record it [#install-it--and-record-it]

    `pip install token-police` / `npm i token-police`, plus any framework extra or companion
    package you need ([supported versions](/docs/sdk/supported-versions)).

    Then **write it into your dependency manifest** — `requirements.txt`, `pyproject.toml`,
    `package.json`. An install that only exists in your shell works today and breaks on the next
    deploy.
  </Step>

  <Step>
    ### Call `init()` at startup, before any LLM client is imported [#call-init-at-startup-before-any-llm-client-is-imported]

    One `init()` per process, at the top of the entrypoint your app actually boots from — not
    inside a request handler.

    The ordering matters because TokenPolice wraps your provider client, and it can only do that
    before the client is constructed. Set three things: the API key (from the environment), the
    collector URL, and `firewall` — start at `"dry_run"`.

    **Node:** also pass your provider modules via `instrumentModules`. It's required on native
    ESM, on `openai` 7.x, and on pnpm/workspace/`file:` installs — and harmless everywhere else,
    so just do it. See [Node & ESM](/docs/sdk/node-esm).
  </Step>

  <Step>
    ### Wrap the entrypoint [#wrap-the-entrypoint]

    Find the function that handles one unit of work — one request, one agent run, one job — and
    wrap it. That's what turns a pile of loose LLM calls into one named, costed run.

    A dynamic loop that decides its own next step is an agent; a fixed pipeline is a chain. An app
    that makes a single call per request still gets wrapped: that's how identity gets attached.

    **Wrap it once.** One wrapper per logical run. Two layers around the same work produce two
    runs, one of them empty.
  </Step>

  <Step>
    ### Thread the identity through [#thread-the-identity-through]

    Pass `user_id`, `paid_plan`, and `session_id` into the wrapper, using the answers from above.

    Normalize the plan value at the point you pass it — it's matched byte for byte, so `"Free"`
    and `"free"` are different plans. And use the *same* `session_id` for every turn of one
    conversation; a fresh id per turn is a broken integration, not a smaller one.

    Details: [Identity](/docs/concepts/identity).
  </Step>

  <Step>
    ### Name the steps in a multi-step loop [#name-the-steps-in-a-multi-step-loop]

    If one run makes several different LLM calls, name each one — `set_span_name("summarize")` /
    `setSpanName("route")` just before the call. Otherwise every step shows up as the model's
    name and you can't tell the retrieval call from the answer call.

    Skip this if your run makes one call. In Node, the call only works **inside** a wrapper — see
    [Naming steps](/docs/sdk/set-span-name).
  </Step>

  <Step>
    ### Wrap tool executions that aren't already captured [#wrap-tool-executions-that-arent-already-captured]

    If your framework runs tools for you, they're probably captured already: LangChain, LlamaIndex,
    Pydantic AI and the OpenAI Agents SDK all emit tool spans on their own.

    Hand-rolled tool loops, MCP tools, CrewAI tools, and **all Vercel AI SDK tools** are not — wrap
    those yourself. Only arg and result hashes and lengths are recorded, never the values.

    Never wrap a tool that's already captured automatically: you'll get the span twice.
  </Step>

  <Step>
    ### Flush, if your process is short-lived [#flush-if-your-process-is-short-lived]

    A long-running server needs nothing here. A serverless function or a script does — it can
    exit before the last usage log is sent.

    * **Serverless:** wrap the handler with `serverless()`, or `await` a flush in a `finally`.
    * **Node scripts and CLIs:** `await tp.shutdown()` at the end. Node's `flushSync()` does *not*
      drain — treating it as a drain is the classic mistake here.
    * **Python scripts:** `tp.flush_sync()`.

    Setting `deployment` does not flush for you. See
    [Flushing & serverless](/docs/sdk/flushing-and-serverless).
  </Step>

  <Step>
    ### Handle `TokenPoliceBlockedError` [#handle-tokenpoliceblockederror]

    Catch it at the boundary you chose earlier — the HTTP handler, the queue consumer, the CLI's
    top level — and turn it into your 429 or 402, or a friendly message.

    Catch **that class only**. Don't add a broad `except`/`.catch` around your business logic "for
    safety": TokenPolice never throws anything else, so a broad catch adds no protection and hides
    your own bugs. See [Errors](/docs/sdk/errors).
  </Step>

  <Step>
    ### Put the key and URL in your environment [#put-the-key-and-url-in-your-environment]

    `TOKENPOLICE_API_KEY` and `TOKENPOLICE_BASE_URL` go through whatever mechanism your repo
    already uses — `.env`, a secret manager, your platform's config. Add the names to
    `.env.example`. Never hardcode the key, never commit it.
  </Step>
</Steps>

## Then prove it works [#then-prove-it-works]

Don't call it done because it compiles. The skill won't, and neither should you.

<Steps>
  <Step>
    ### Read the boot output [#read-the-boot-output]

    Start the app with `log_errors=True` / `logErrors: true` and read what the SDK prints. Three
    warnings actually matter:

    * **"could not attach enforcement to it"** — the firewall will never run for that package.
      Fix it with `instrumentModules`.
    * **"patch() failed … will NOT be metered"** — that copy of the provider library is invisible.
    * **"token capture … is not yet supported"** — the installed provider major is outside what
      auto-discovery handles. On `openai` 7.x, pass the module ([supported
      versions](/docs/sdk/supported-versions)).

    A clean boot is the single best signal you wired it right.
  </Step>

  <Step>
    ### Exercise the app, then read the dashboard [#exercise-the-app-then-read-the-dashboard]

    Run it the way a user would: a few normal requests, a multi-turn conversation, a tool-using
    run, a streamed request, and one of each non-chat call you make. Then check
    [app.tokenpolice.ai](https://app.tokenpolice.ai) for:

    * Calls appearing at all.
    * The right `user_id` and plan — not `anonymous` / `free` everywhere.
    * Conversations grouping across turns, rather than every turn showing up alone.
    * Span names that are your step names, not model names.
    * The provider column matching the endpoint you actually called. Gemini or Groq traffic sent
      through an OpenAI-compatible URL should not say `openai`.
    * Non-zero token counts on streamed calls.
  </Step>

  <Step>
    ### Test the failure you hope never happens [#test-the-failure-you-hope-never-happens]

    Point `base_url` at somewhere unreachable — `http://127.0.0.1:9` — send one normal request,
    and confirm your app answers it normally. Then put the real URL back.

    That proves fail-open: if TokenPolice goes down, your app doesn't. &#x2A;*Do this before you ship
    on enforce.** The one thing that *should* still block is something the firewall had already
    blocked — that's deliberate ([fail-open](/docs/concepts/fail-open)).
  </Step>

  <Step>
    ### Turn the debugging back off [#turn-the-debugging-back-off]

    Remove `log_errors` / `logErrors`, any temporary prints, and any log level you raised. Ship
    with the same logging posture the app had before you started.
  </Step>
</Steps>

## Next [#next]

<Cards>
  <Card title="Manual setup — Python" href="/docs/get-started/manual-python" description="The code for each step above, in Python." />

  <Card title="Manual setup — Node" href="/docs/get-started/manual-node" description="The code for each step above, in Node / TypeScript." />

  <Card title="Dry-run vs enforce" href="/docs/concepts/dry-run-vs-enforce" description="When you're ready to let a rule actually block." />
</Cards>
