TokenPolice
Docs
Core concepts

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

WhatExample
Model and providergpt-4o-mini, openai
Token counts812 in, 240 out, cached counts where the provider reports them — TokenPolice computes the USD cost from these on its side
The identity tags you choseuser_id, paid_plan, session_id
Names you gave the workthe workflow/session name, and any span name you set
The kind of callchat, embedding, image, speech, transcription, video, OCR
Tool call shapethe tool's name, plus hashes and lengths of its arguments and result — never the values
A content fingerprinta hash, used for loop detection (below)
The provider endpoint you calledhttps://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, verbatimon 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 callhow much depends on error_detail (below)
Any metadata you attachwhatever 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:

ModeWhat 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.

Next