# How privacy works

How AnonRouter separates identity from content, runs the content plane in an attested confidential VM, and where compatibility mode changes the guarantee.

Most model gateways authenticate you and forward your prompt from the same
place. That one system can associate your identity with your content. AnonRouter
offers a private ticket flow that separates those roles before the request
leaves your client, and it never intentionally writes prompts or responses to
disk.

This page is the whole picture in one place: the idea, the ticket that makes it
work, the request lifecycle, the guarantees, and where those guarantees stop.

## The core idea: separate who from what

A private router is not about promising to be careful with your data. It is
about arranging the system so the sensitive join never exists in the first
place.

In the private ticket flow, AnonRouter splits a request across roles that each
hold only one half of it:

* The **control plane** knows **who you are** (your key, balance, and policy).
  It never receives your prompt.
* The **relay** sees **what you sent** (the prompt and response) but never learns
  your key or account.
* The **provider worker** holds the **provider credential** and the authorized
  payload, but never sees your account.

Because identity lives on one side and content on the other, neither side can
say who sent which prompt. The single-use ticket is what lets them cooperate
without ever introducing you to each other.

### The content side runs in a confidential VM

Separating the roles answers &#x2A;"can any one component link you to a prompt?"&#x2A; It
does not by itself answer &#x2A;"what stops the content side from keeping the prompt
anyway?"*

The relay, compatibility broker, and provider workers run inside an &#x2A;*Intel TDX
confidential VM (CVM)**. TLS for `api.anonrouter.ai` terminates inside that CVM,
not at an ordinary proxy in front of it. The host operator cannot inspect the
running VM's memory to read prompts.

The part that matters is that this is **checkable rather than promised**: the
plane will attest to itself, credential-free, to anyone who asks.

```bash
NONCE=$(openssl rand -hex 32)
curl -s "https://api.anonrouter.ai/v1/gateway/attestation?nonce=$NONCE"
```

See [verify AnonRouter itself](/tee-verification#verify-anonrouter-itself-hop-1)
for what that document proves and how to check it without writing a verifier.

<small>
  On a narrow screen, scroll the diagram horizontally to follow the control plane, confidential content plane, and provider boundary.
</small>

<div style="{ overflowX: &#x22;auto&#x22;, margin: &#x22;1.75rem 0&#x22;, paddingBottom: &#x22;0.5rem&#x22; }">
  <svg role="img" aria-label="AnonRouter privacy architecture. A client sends its API key and content-free request limits to the AnonRouter control plane and receives a one-use ticket. It sends that ticket and its prompt to the attested Intel TDX content plane at api.anonrouter.ai. The content plane calls the selected provider and returns usage metadata, but not content, for settlement. Compatibility mode sends the key and prompt together to a broker inside the confidential VM and therefore does not provide the same unlinkability." viewBox="0 0 960 680" style="{ width: &#x22;100%&#x22;, minWidth: &#x22;760px&#x22;, maxWidth: &#x22;960px&#x22;, height: &#x22;auto&#x22;, display: &#x22;block&#x22;, margin: &#x22;0 auto&#x22; }">
    <defs>
      <marker id="privacy-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
        <path d="M0 0 L10 5 L0 10 z" fill="currentColor" />
      </marker>
    </defs>

    <rect x="20" y="20" width="920" height="640" rx="18" fill="currentColor" fillOpacity="0.018" stroke="currentColor" strokeOpacity="0.16" />

    <rect x="352" y="42" width="256" height="76" rx="14" fill="currentColor" fillOpacity="0.04" stroke="currentColor" strokeOpacity="0.45" />

    <text x="480" y="73" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="18" fontWeight="700" fill="currentColor">
      Your client
    </text>

    <text x="480" y="97" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="12.5" fill="currentColor" fillOpacity="0.68">
      the privacy split happens here
    </text>

    <rect x="54" y="188" width="340" height="220" rx="16" fill="#3b82f6" fillOpacity="0.055" stroke="#3b82f6" strokeWidth="1.8" />

    <text x="78" y="222" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="12" fontWeight="700" fill="#3b82f6">
      IDENTITY AND CONTROL PLANE
    </text>

    <text x="78" y="252" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="19" fontWeight="700" fill="currentColor">
      control.anonrouter.ai
    </text>

    <text x="78" y="281" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="13" fill="currentColor" fillOpacity="0.76">
      Knows your key, account, balance, and policy.
    </text>

    <text x="78" y="304" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="13" fill="currentColor" fillOpacity="0.76">
      Receives model and priced limits, never the prompt.
    </text>

    <rect x="78" y="329" width="292" height="52" rx="10" fill="#3b82f6" fillOpacity="0.08" stroke="#3b82f6" strokeOpacity="0.55" />

    <text x="224" y="351" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="13" fontWeight="700" fill="currentColor">
      Mints one-use opaque ticket
    </text>

    <text x="224" y="370" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fill="currentColor" fillOpacity="0.68">
      bound to model · limits · policy
    </text>

    <rect x="566" y="162" width="340" height="308" rx="16" fill="#10b981" fillOpacity="0.055" stroke="#10b981" strokeWidth="1.8" />

    <text x="590" y="196" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="12" fontWeight="700" fill="#10b981">
      INTEL TDX · CONFIDENTIAL VM
    </text>

    <text x="590" y="226" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="19" fontWeight="700" fill="currentColor">
      api.anonrouter.ai
    </text>

    <text x="590" y="249" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="12.5" fill="currentColor" fillOpacity="0.68">
      TLS terminates inside the attested CVM
    </text>

    <rect x="590" y="274" width="292" height="62" rx="10" fill="#10b981" fillOpacity="0.08" stroke="#10b981" strokeOpacity="0.55" />

    <text x="736" y="299" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="14" fontWeight="700" fill="currentColor">
      Ticket relay
    </text>

    <text x="736" y="320" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fill="currentColor" fillOpacity="0.68">
      prompt + ticket · no stable key or account
    </text>

    <rect x="590" y="348" width="292" height="62" rx="10" fill="#10b981" fillOpacity="0.08" stroke="#10b981" strokeOpacity="0.55" />

    <text x="736" y="373" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="14" fontWeight="700" fill="currentColor">
      Provider workers
    </text>

    <text x="736" y="394" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fill="currentColor" fillOpacity="0.68">
      isolated credentials · model-bound payload
    </text>

    <rect x="590" y="422" width="292" height="28" rx="8" fill="#f59e0b" fillOpacity="0.08" stroke="#f59e0b" strokeOpacity="0.55" />

    <text x="736" y="441" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fontWeight="650" fill="currentColor">
      Optional compatibility broker lives here too
    </text>

    <path d="M400 118 C306 137 244 151 224 182" fill="none" stroke="#3b82f6" strokeWidth="1.8" markerEnd="url(#privacy-arrow)" />

    <text x="276" y="137" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fontWeight="650" fill="#3b82f6">
      1 · key + content-free limits
    </text>

    <path d="M256 188 C308 152 362 138 421 117" fill="none" stroke="#3b82f6" strokeWidth="1.8" strokeDasharray="5 5" markerEnd="url(#privacy-arrow)" />

    <text x="341" y="171" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fontWeight="650" fill="#3b82f6">
      2 · opaque ticket
    </text>

    <path d="M540 118 C630 128 694 138 719 156" fill="none" stroke="#10b981" strokeWidth="1.8" markerEnd="url(#privacy-arrow)" />

    <text x="654" y="128" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fontWeight="650" fill="#10b981">
      3 · ticket + prompt
    </text>

    <path d="M512 100 C568 83 639 82 707 107" fill="none" stroke="currentColor" strokeOpacity="0.42" strokeWidth="1.5" strokeDasharray="4 5" markerEnd="url(#privacy-arrow)" />

    <text x="624" y="75" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11" fill="currentColor" fillOpacity="0.68">
      verify attestation before sending
    </text>

    <rect x="566" y="526" width="340" height="86" rx="16" fill="#a855f7" fillOpacity="0.05" stroke="#a855f7" strokeWidth="1.6" />

    <text x="736" y="555" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="12" fontWeight="700" fill="#a855f7">
      EXTERNAL MODEL PROVIDER
    </text>

    <text x="736" y="581" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="13" fill="currentColor" fillOpacity="0.76">
      Sees plaintext on ordinary routes.
    </text>

    <text x="736" y="600" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fill="currentColor" fillOpacity="0.64">
      Provider E2EE routes keep it encrypted.
    </text>

    <line x1="736" y1="470" x2="736" y2="519" stroke="#a855f7" strokeWidth="1.8" markerEnd="url(#privacy-arrow)" />

    <text x="762" y="501" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fill="#a855f7">
      4 · provider call
    </text>

    <path d="M566 448 C484 494 390 485 330 416" fill="none" stroke="currentColor" strokeOpacity="0.45" strokeWidth="1.5" strokeDasharray="5 5" markerEnd="url(#privacy-arrow)" />

    <text x="444" y="470" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fill="currentColor" fillOpacity="0.68">
      5 · usage metadata only for settlement
    </text>

    <rect x="54" y="526" width="340" height="86" rx="16" fill="#f59e0b" fillOpacity="0.05" stroke="#f59e0b" strokeWidth="1.6" />

    <text x="224" y="555" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="12" fontWeight="700" fill="#f59e0b">
      COMPATIBILITY MODE
    </text>

    <text x="224" y="581" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="12.5" fill="currentColor" fillOpacity="0.76">
      One URL, but key and prompt meet in the CVM.
    </text>

    <text x="224" y="600" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fill="currentColor" fillOpacity="0.64">
      Host-blind, but not unlinkable like the ticket flow.
    </text>

    <text x="480" y="642" textAnchor="middle" fontFamily="ui-sans-serif, system-ui, sans-serif" fontSize="11.5" fill="currentColor" fillOpacity="0.58">
      No prompt or response is intentionally persisted by AnonRouter.
    </text>
  </svg>
</div>

## What a ticket is

A ticket is a short-lived, single-use authorization that stands in for your
identity. You spend your key once, with the control plane, to obtain a ticket;
you then hand the ticket (not your key) to the relay that actually serves the
request.

Every ticket is:

* **Opaque.** It carries no readable account information.
* **Single-use.** It is redeemed exactly once and then rejected forever.
* **Short-lived.** It expires in roughly 30 seconds, so a leaked ticket has
  almost no window.
* **Request-bound.** It is minted for a specific model, a specific output-token
  limit, and a digest of the request's routing and reasoning policy. A ticket
  cannot be replayed against a different or larger request than the one it was
  issued for.

## The lifecycle of a request

1. **Authenticate once, privately.** Your client calls the control plane at
   `POST /v1/inference/tickets` with your inference key, naming the model and
   output limit. The control plane verifies the key and confirms you can afford
   the request. See [authentication](/authentication) and
   [the quickstart](/quickstart) for the exact calls.
2. **Reserve, then mint.** The control plane reserves the **worst-case** cost of
   the request (the most it could cost at the requested output limit) against
   your balance, then mints a single-use ticket bound to that request. This is
   the last point at which your identity and the request meet.
3. **Redeem at the relay.** Your client sends the completion to the relay with
   the ticket in the `x-anonrouter-ticket` header. The relay redeems the ticket
   once, checks that the request matches what the ticket was bound to, and never
   receives your key or account.
4. **Fence each provider attempt.** Before any provider call, the attempt is
   fenced with its own single-use dispatch authorization. If the provider fails
   before any output, [failover](/provider-routing#automatic-failover) may
   authorize one more attempt on a different provider, but only within the plan
   the ticket bound: within the workspace's retry limit (never more than 3
   attempts in all), never below the privacy floor, within the amount already
   reserved, and never for an E2EE request. A ticket cannot fan out beyond that
   bound, and a replayed or altered attempt is refused. Each fallback does send
   the request to one more provider, which is why a workspace can limit retries
   or turn fallback off.
5. **Serve.** The provider worker, holding only the provider credential and the
   authorized payload, calls the model provider and streams the response back
   through the relay to you.
6. **Settle.** Billing is finalized from usage metadata (token counts, status,
   latency) only. The reserved amount is settled to the real cost and the
   remainder released. Content is never read to bill. See
   [metering and settlement](/metering).

## Security properties

| Property                     | How it is enforced                                                                                                                                                                                                                                                                            |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unlinkability                | Identity is held only by the control plane; content is held only by the relay. Neither can join the two, so no record maps you to a prompt.                                                                                                                                                   |
| Single use and replay safety | A ticket is redeemed exactly once and then permanently rejected. A replayed or expired ticket is refused before any provider work.                                                                                                                                                            |
| Request binding              | The ticket is bound to the model, the output-token limit, and a policy digest. It cannot be upgraded to a bigger or different request after issuance.                                                                                                                                         |
| Worst-case reservation       | Funds are held for the maximum the request could cost before the first attempt, so serving never needs to consult your balance or identity again.                                                                                                                                             |
| No content at rest           | Prompts, responses, system prompts, files, tool arguments, and raw provider bodies are never written to disk. Keys are stored only as a hash.                                                                                                                                                 |
| Confidential execution       | The relay, the compatibility broker and every provider worker run inside an Intel TDX confidential VM that terminates TLS in-enclave, so content never reaches a host an operator can read it off. [Verifiable on demand](/tee-verification#verify-anonrouter-itself-hop-1), credential-free. |
| Fail closed                  | If a privacy or authorization check cannot be satisfied, the request is refused rather than served on a weaker footing.                                                                                                                                                                       |

## What is and is not retained

Operational records may contain account and credential identifiers, provider and
model identifiers, token counts, estimated and final cost, request status and
latency, request identifiers and timestamps, and billing, security, and
rate-limit metadata.

AnonRouter does not intentionally persist prompt or response text, system
prompts, uploaded file contents, tool arguments or outputs, raw provider request
or response bodies, or API keys, session cookies, and authorization headers. API
keys are shown once at creation and kept only as a hash.

## Privacy classifications

Separating identity from content protects you at the router. It does not change
what the model provider on the other end can do with the prompt while serving
it. That is what the per-route privacy label tells you. Every route carries
exactly one classification, and automatic routing never silently widens the set
you allowed.

| Label     | `privacy_class` | What it means                                                                                                              |
| --------- | --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Anonymous | `anonymous`     | Your identity is hidden, but the model provider can see the prompt while serving it. Provider retention is not guaranteed. |
| Private   | `private`       | Prompt and response content is not retained after the request completes.                                                   |
| TEE       | `tee`           | Inference runs inside a hardware-isolated, attestable enclave.                                                             |
| E2EE      | `e2ee`          | Your client encrypts the prompt so that only a verified enclave can decrypt it.                                            |

> **Two layers, read both**
>
> The ticket flow decides whether AnonRouter can link you to a request. The
> privacy label decides what the provider serving the request can see. A private
> request over an Anonymous route is still unlinkable at the router, but the
> provider can read the prompt. See [models and privacy labels](/models) for the
> label on each route, the [E2EE client guide](/e2ee) for encrypted request
> setup, and [TEE verification and encrypted inference](/tee-verification) for
> enclave evidence, verification levels, and signatures.

## Compatibility mode: an explicit trade-off

Some clients cannot perform the two-step ticket exchange. [Compatibility
mode](/compatibility) lets those clients send a static key straight to the API
with no ticket. To do that, a broker must hold your key identity and your
plaintext request together in memory long enough to mint a ticket internally and
forward the request.

That broker logs metadata only and holds no database or provider credentials,
but this flow is **not unlinkable** the way the ticket flow is, because your
identity and your content briefly meet in one place. OpenAI-compatible access is
enabled by default on newly created keys so ordinary tools work immediately. You
can turn it off while creating a key when that key will use only the private
ticket flow.

## How this is different from a typical router

A conventional gateway is a trusted middleman: it authenticates you and
forwards your prompt from the same service,
so it can always tie your account to your content, and you rely on its policy
not to keep that link.

In AnonRouter's private ticket flow, the link is not merely unlogged, it is
architecturally absent: the component that knows you never sees your prompt,
and the component that sees your prompt never knows you. The ticket is the
mechanism that lets those two halves serve one request without ever being
introduced.

And the half that does see your prompt runs inside a confidential VM that will
prove what it is running, to anyone, without a credential. So "we do not keep
your prompts" is not a policy you have to believe. It is a property you can
check, and one we would have to visibly break to violate.

> **Two things this does not claim**
>
> **The provider still sees your prompt** on an ordinary route. Our worker
> forwards it upstream, where it is handled under that provider's policy. The
> [privacy label on each route](/models) is what tells you which, and an
> [E2EE route](/e2ee) is what avoids it.
> 
> **Inside the enclave, our attested build handles your plaintext** in order to
> route and meter it. A build changed to exfiltrate it would change the
> measurements and stop verifying, so cheating is *detectable* rather than
> impossible. E2EE is the mode where our build is not in your trust set at all.

> **Try it**
>
> Walk the full flow yourself in [the quickstart](/quickstart), or read the
> per-route guarantees in [models and privacy labels](/models).
