Do it yourself, the way the skill does it
The same steps the TokenPolice skill performs, in order, with the reasoning behind each one and the questions to answer before you start.
The coding-agent skill 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 and Manual setup — Node.
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. |
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.
The checklist
Install it — and record it
pip install token-police / npm i token-police, plus any framework extra or companion
package you need (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.
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.
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.
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.
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.
Wrap tool executions that aren't 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.
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(), orawaita flush in afinally. - Node scripts and CLIs:
await tp.shutdown()at the end. Node'sflushSync()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.
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.
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.
Then prove it works
Don't call it done because it compiles. The skill won't, and neither should you.
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
openai7.x, pass the module (supported versions).
A clean boot is the single best signal you wired it right.
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 for:
- Calls appearing at all.
- The right
user_idand plan — notanonymous/freeeverywhere. - 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.
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. 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).
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.

