Data & Privacy Architecture

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.

The short answer

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.

What the SDK transmits

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:

FieldTypeWhat it holds
agent_namestringThe label you pass to track_costs.agent()
modelstringModel identifier, e.g. claude-sonnet-4
input_tokensintReported by the provider, or counted with tiktoken
output_tokensintReported by the provider, or counted with tiktoken
total_tokensintSum of the two above
costfloatComputed locally from the pricing table
latency_msintWall-clock duration of the call
timestampISO 8601UTC time the call completed
successboolWhether the call raised
errorstring | nullThe provider exception message — see the caveat below
input_hashSHA-256 hexOne-way digest of the prompt, never the prompt
streamingboolPresent only on streamed calls
metadataobjectOnly 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:

FieldTypeWhat it holds
trace_idrandom hexGenerated per run; means nothing outside your project
span_idrandom hexGenerated per call
parent_span_idrandom hexWhich span this call sits under
workflowstringThe name you pass to workflow()
step_namestringThe name you pass to step() or tool()
tool_namestringThe name you pass to tool()
step_indexintOrdinal of the step within the run
depthintHow deeply the call was nested

Calling outcome() adds one more record per run — not per call — carrying only this:

FieldTypeWhat it holds
trace_idrandom hexWhich run this outcome belongs to
workflowstringThe name you passed to workflow()
successboolWhether you called it a success
labelstringOptional 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.

What never leaves your process

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:

  • Prompt and message text
  • Model completions and responses
  • System prompts and instructions
  • Tool definitions, tool arguments, tool results
  • Reasoning and thinking blocks
  • Skill files, config files, or any file on disk
  • Your LLM provider API keys
  • Embeddings, documents, or retrieval context

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.

How prompts are handled

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.

python
# 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.

The fields carrying content you control

Three things you write can reach us as free text. All are worth understanding before you deploy.

metadata

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.

python
# 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)

error

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.

workflow, step_name, tool_name, label

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().

Three deployment modes

Pick the one matching your risk tolerance. All three run the same SDK and produce the same analytics.

ModeLeaves your networkSetup
CloudMetadata events onlyAPI key + project ID
Local mode Nothinglocal_mode=True
Self-hosted NothingPoint 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:

python
from agentcost import track_costs

track_costs.init(local_mode=True)

# ... run your agent ...

events = track_costs.get_local_events()   # never left the process

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

Credentials and secrets

  • AgentCost API keys are stored as SHA-256 digests. The plaintext key is shown once at creation and never persisted, so a database disclosure does not yield usable keys.
  • Account passwords are hashed with bcrypt through passlib, with per-password salting and deliberate slowness against offline cracking.
  • LLM provider keys are never read by the SDK. It wraps the method on your client object; the credential stays where you configured it.
  • Transport is HTTPS to api.agentcost.tech, authenticated with a bearer token scoped to a single project.

Retention and deletion

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.

Verify this yourself

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:

  • anthropic_interceptor.py — see _build_event for the complete transmitted payload. The OpenAI, Gemini, and LangChain interceptors build the identical shape.
  • http_client.py — the only place the SDK opens a socket.
  • trace.py — every trace field, and the fact that none are produced outside a workflow().
  • tracker.py — local mode swapping the HTTP client for an in-process stub.
  • admin_service.py 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.

Questions we have not answered

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.