Exactly what AgentCost collects, what never leaves your process, and how to verify both yourself.
This is the engineering companion to our Privacy Policy. The policy states our commitments; this page shows the code that implements them.
AgentCost is a metadata-only tracker. The SDK sends token counts, model names, cost, latency, timing, and — if you ask for it — the shape of a multi-step run. It does not send your prompts, your completions, your system instructions, or your files — not by default, and not behind a setting. There is no configuration in which prompt content is transmitted, because the SDK never puts it on the wire in the first place.
If that is still more than you want to share, two stronger options exist: local mode, where nothing leaves your process at all, and self-hosting, where you run the entire platform.
One event is emitted per LLM call. Events are batched and sent to a single endpoint — POST /v1/events/batch — alongside your project ID and API key. That endpoint is the SDK's only network egress. The complete event schema is:
| Field | Type | What it holds |
|---|---|---|
| agent_name | string | The label you pass to track_costs.agent() |
| model | string | Model identifier, e.g. claude-sonnet-4 |
| input_tokens | int | Reported by the provider, or counted with tiktoken |
| output_tokens | int | Reported by the provider, or counted with tiktoken |
| total_tokens | int | Sum of the two above |
| cost | float | Computed locally from the pricing table |
| latency_ms | int | Wall-clock duration of the call |
| timestamp | ISO 8601 | UTC time the call completed |
| success | bool | Whether the call raised |
| error | string | null | The provider exception message — see the caveat below |
| input_hash | SHA-256 hex | One-way digest of the prompt, never the prompt |
| streaming | bool | Present only on streamed calls |
| metadata | object | Only what you explicitly attach — see below |
If you group a multi-step run with workflow(), step() or tool(), each event also carries where it sat in that run. Instrument nothing and none of these fields are sent at all:
| Field | Type | What it holds |
|---|---|---|
| trace_id | random hex | Generated per run; means nothing outside your project |
| span_id | random hex | Generated per call |
| parent_span_id | random hex | Which span this call sits under |
| workflow | string | The name you pass to workflow() |
| step_name | string | The name you pass to step() or tool() |
| tool_name | string | The name you pass to tool() |
| step_index | int | Ordinal of the step within the run |
| depth | int | How deeply the call was nested |
Calling outcome() adds one more record per run — not per call — carrying only this:
| Field | Type | What it holds |
|---|---|---|
| trace_id | random hex | Which run this outcome belongs to |
| workflow | string | The name you passed to workflow() |
| success | bool | Whether you called it a success |
| label | string | Optional label you choose, e.g. "resolved" |
Those three tables are the entire payload. The ids are random and carry no meaning outside your project. The only free text is error, the metadata you attach, and the workflow, step, tool and outcome-label names you write yourself — all documented below.
The SDK intercepts your provider client to read token usage off the response object. It reads the request in memory to count tokens and compute a hash, then discards it. None of the following is transmitted, logged, or stored:
Your provider API keys are a special case: the SDK wraps the client's method, not its credentials. It never reads, stores, or transmits the key you authenticate to OpenAI, Anthropic, or Google with.
No LLM sits in the path of your data on our side either. Optimization recommendations are produced by deterministic analysis over your own usage statistics — there is no model call, and consequently nothing to leak into one.
To detect repeated calls — the signal behind caching recommendations — the SDK needs to know when two prompts are identical, without knowing what they say. It normalizes the request text, takes a SHA-256 digest, and transmits only the digest. The text itself never leaves the function.
# agentcost/anthropic_interceptor.py
def _hash_input(text: str) -> str:
normalized = " ".join(text.split()).lower().strip()
return hashlib.sha256(normalized.encode()).hexdigest()Hashing is one-way: the digest cannot be reversed into the prompt. We want to be precise about the limit of that guarantee, though.
An honest caveat about hashes
A SHA-256 digest is irreversible, but it is not a secret if the input is guessable. Anyone who can enumerate a small space of candidate prompts can confirm a match by hashing them. For long or unique prompts this is infeasible; for a short prompt drawn from a known set, a hash confirms membership.
We think this is the right trade for duplicate detection, and we would rather state the limit than imply hashing is absolute. If your prompts are short and drawn from a predictable set, use local mode or self-host.
Three things you write can reach us as free text. All are worth understanding before you deploy.
Whatever you attach through track_costs.metadata() is transmitted verbatim. This is the one place you can send us sensitive data, and it is entirely under your control. Use opaque identifiers rather than personal information.
# Good — opaque identifiers
with track_costs.metadata(user_id="u_8fc21a", tenant="acme"):
llm.invoke(prompt)
# Avoid — personal data in metadata
with track_costs.metadata(email="person@example.com"):
llm.invoke(prompt)When a call fails, the SDK records the provider's exception message so failures show up in your dashboard. That string comes from the provider SDK, not from us. Most provider errors are generic — rate limits, timeouts, auth failures — but some classes of error, content-policy rejections in particular, can quote a fragment of the offending input back to you. If that matters for your workload, local mode and self-hosting both keep the string on your infrastructure.
These are labels you write, and we would rather point out what they can reveal than let you discover it later. They describe nothing about your data, but they do describe your architecture: a step called screen_applicant_credit_risk tells us more about your product than any token count ever will. That may be entirely fine — most teams name steps after obvious engineering stages — but it is a deliberate choice rather than an accident, so it belongs on this page.
Name steps after what the code does rather than what the business is doing, and nothing sensitive travels. Or skip the trace API entirely: none of these fields exist on your events unless you open a workflow().
Pick the one matching your risk tolerance. All three run the same SDK and produce the same analytics.
| Mode | Leaves your network | Setup |
|---|---|---|
| Cloud | Metadata events only | API key + project ID |
| Local mode | Nothing | local_mode=True |
| Self-hosted | Nothing | Point the SDK at your own backend |
In local mode the HTTP client is replaced with an in-process stub. No API key is required, no socket is opened, and events stay retrievable in memory:
from agentcost import track_costs
track_costs.init(local_mode=True)
# ... run your agent ...
events = track_costs.get_local_events() # never left the processFor self-hosting, set AGENTCOST_API_URL (or pass base_url) to your own deployment. The backend is open source and ships with a Dockerfile and compose file.
api.agentcost.tech, authenticated with a bearer token scoped to a single project.While your account is active, usage events are retained indefinitely. This is deliberate: cost trends, baselines, and anomaly detection are only meaningful against long history, and truncating it would silently degrade the product. Events hold no prompt content, so what accumulates is a numeric time series.
Deleting your account starts a 7-day grace period, after which a scheduled job hard-deletes your data. Deletion is explicit rather than reliant on database cascades — the purge removes events, daily aggregates, optimization recommendations, project baselines, input pattern caches, pending invitations, and the projects themselves, then revokes every active session.
Self-hosted deployments set their own retention: the data is in your database and never reaches ours.
Every claim on this page is checkable against source. The SDK and backend are both open source under the MIT license. The files that matter:
_build_event for the complete transmitted payload. The OpenAI, Gemini, and LangChain interceptors build the identical shape.workflow().delete_user_permanently, the deletion path described above.You can also watch the wire directly. Run your agent against a local proxy, or start in local mode and inspect get_local_events() — the structure is the same one that would have been transmitted.
If you are evaluating AgentCost against a security review and need something this page does not cover, ask. We would rather answer a hard question directly than have you infer the answer.
Email hello@agentcost.tech or open an issue on the SDK repository.