Data & privacy
Exactly what TokenPolice receives from your app, what it never receives, and the one caveat on the promise.
TokenPolice works on token metadata. The text of your prompts and the model's completions never reaches TokenPolice — not on the check before a call, not on the log after it. On Python there is one path by which that text can reach a third party of your own choosing; it is the callout under Where it goes, and it is worth reading before you deploy.
Instrumentation runs inside your own process. It reads the shape of each call, not its content, and sends a small record to the collector.
What leaves your process
| What | Example |
|---|---|
| Model and provider | gpt-4o-mini, openai |
| Token counts | 812 in, 240 out, cached counts where the provider reports them — TokenPolice computes the USD cost from these on its side |
| The identity tags you chose | user_id, paid_plan, session_id |
| Names you gave the work | the workflow/session name, and any span name you set |
| The kind of call | chat, embedding, image, speech, transcription, video, OCR |
| Tool call shape | the tool's name, plus hashes and lengths of its arguments and result — never the values |
| A content fingerprint | a hash, used for loop detection (below) |
| The provider endpoint you called | https://api.groq.com/openai/v1 — scheme, host and path only. Credentials, query string and fragment are stripped before it is sent. Sent as model_extras.api_base |
| Audio file names, verbatim | on speech and transcription calls the file's own name is sent as-is — intake-call.mp3. It is not hashed. See the warning below |
| Error class on a failed call | how much depends on error_detail (below) |
Any metadata you attach | whatever you put there — see the note below |
What never leaves your process
- Your prompts, system prompts, messages, and retrieved context.
- The model's completions, in whole or in part.
- Tool arguments and tool results as values.
- Your provider API keys.
The content fingerprint
Loop detection needs to know when an agent is asking the model the same thing over and over. To do that without seeing the question, the SDK sends a hash of the call's content — a fixed-length fingerprint. Two identical prompts produce the same fingerprint.
The hash is a truncated SHA-1: 16 hex characters, with no salt. It cannot be reversed — there is no procedure that reads the text back out of it. Be precise about what that buys you, though. Agent prompts are templated and low-entropy by construction, so someone who already holds a candidate prompt can hash it themselves and see whether it matches. The fingerprint hides your content from a party starting with nothing; it does not stop a party starting with a good guess from confirming the guess. That is the whole caveat on "text never leaves": a one-way hash of it does, and only so a runaway loop can be spotted.
Error detail
When a provider call fails, the SDK records why. The error_detail option in
init() decides how much of that leaves your process:
| Mode | What is sent |
|---|---|
"none" | A classification only — no message text, no hash. |
"redacted" (default) | The exception's class name, plus a hash of the message. |
"raw" | The verbatim error string, truncated. |
"raw" is opt-in for a reason: a provider's 400 response sometimes quotes the offending part
of your prompt back at you. Turn it on only when you're debugging, and turn it off after.
metadata is the one field you can over-share with. Everything else is metadata by
construction; metadata is free-form, so whatever you put in it is what gets sent. Use it
for tags you'd be comfortable seeing in a dashboard — tenant_id, feature, region — and
keep user content and secrets out of it.
Audio file names are sent unhashed. On a speech or transcription call the SDK reads the
file's name and sends it as the entry name. File names routinely carry more than you think —
jane-doe-intake-call.mp3. If your pipeline names files after people, cases or accounts,
pass the audio under a neutral name (or a stream/buffer with no name) before it reaches the
provider client.
Where it goes
To the collector at https://collect.tokenpolice.ai, over HTTPS, authenticated with your
tp_sk_… key — or to your own address if you point the SDK
elsewhere. That record — the table above — is everything
TokenPolice receives, and TokenPolice sends it nowhere else.
Python: check your own OpenTelemetry setup before you turn this on.
The Node SDK builds a private tracer provider with no exporters and binds its instrumentors to that, so its spans have nowhere to go but TokenPolice's own record.
The Python SDK behaves differently when your app has already configured a
TracerProvider — Datadog, Honeycomb, Langfuse, Logfire, Sentry tracing, or a plain OTLP
exporter. In that case TokenPolice attaches to your provider instead of building its
own, and the OpenLLMetry instrumentors underneath it bind to the global provider too. Those
instrumentors emit gen_ai.prompt.* and gen_ai.completion.* attributes carrying full
prompt and completion text, and that behaviour is on by default.
None of that text goes to TokenPolice. But it will start flowing into your own tracing backend, which may not have been receiving it before — and if that backend is a third-party SaaS, the text has left your servers.
To prevent it, set TRACELOOP_TRACE_CONTENT=false in the environment before tp.init().
The SDK does not set it for you. With it set, the spans still carry model, token counts and
timing; only the content attributes are dropped.
If your security review needs more than this page covers, write to hello@tokenpolice.ai and ask.

