End-to-end encrypted inference
Attest an enclave, encrypt locally, relay provider-bound ciphertext through AnonRouter, and decrypt Chutes, NEAR AI, or Venice responses.
Use E2EE when your application must keep prompts and model output opaque to AnonRouter. Your client verifies fresh enclave evidence, encrypts to the key in that evidence, and sends only ciphertext plus fixed routing metadata. Plaintext is recovered only by the selected model enclave and your client.
E2EE and hardware verification are different claims
A working encrypted transport proves that AnonRouter relayed ciphertext; the
attestation level says how strongly the destination key was verified. Current
Chutes, NEAR AI, and Venice E2EE routes reach provider-attested, not
hardware-verified. Do not encrypt unless the evidence, nonce, route,
measurements, and encryption key all pass the checks your policy requires.
Provider support
| Provider | AnonRouter E2EE transport | Current attestation level | Streaming | Public request receipt |
|---|---|---|---|---|
| Chutes | ML-KEM-768 whole-body ciphertext | provider-attested | No | Provider does not expose one |
| NEAR AI direct | X25519/XChaCha20 field ciphertext, version 2 | provider-attested | Yes | Not for the current streaming E2EE call |
| Venice | secp256k1/AES-GCM field ciphertext | provider-attested | Yes | Yes, including completed streams |
| Tinfoil | Not exposed through AnonRouter | TEE reaches sdk-verified | — | Provider does not expose one |
Tinfoil's upstream EHBP transport encrypts the model selector. Its documented outer metadata does not let AnonRouter prove that the encrypted model is the one bound to the ticket and reservation. AnonRouter therefore fails closed instead of advertising Tinfoil as E2EE.
The safe request sequence
Every E2EE request follows the same security order:
- Select one exact canonical model and provider route. E2EE never uses Auto.
- Generate fresh client key material and a fresh 32-byte attestation nonce.
- Fetch enclave evidence through the appropriate flow below.
- Verify the raw evidence independently, including its nonce, endpoint,
measurements, debug state, and encryption-key binding. Enforce the minimum
verification_levelyour application accepts. - Mint a short-lived inference ticket for that same model and provider with
e2ee: true. - Encrypt locally and send the ticket, fixed E2EE headers, and ciphertext to the relay. Do not send your API key to the relay in a split deployment.
- Decrypt the response locally. If a request fails, repeat from step 2 with fresh evidence, key material, nonces, and tickets.
AnonRouter never falls back an E2EE request to another provider, endpoint, or plaintext route. That is essential: a key attested for one enclave cannot be safely reused for a different destination.
The examples use this base URL:
export ANONROUTER_BASE_URL="https://api.anonrouter.ai/v1"
export ANONROUTER_API_KEY="ar_REPLACE_WITH_YOUR_API_KEY"1. Select an exact E2EE route
Read the live catalog before encrypting:
curl "$ANONROUTER_BASE_URL/models" \
-H "Authorization: Bearer $ANONROUTER_API_KEY"Choose a route whose privacy_class is e2ee, and retain its canonical model,
provider, context window, and maximum output. Use the canonical model in the
ticket and public chat request. Never infer E2EE support from a -TEE suffix.
2. Fetch and verify attestation
Public verified attestation
Chutes and Venice clients can fetch a normalized server verdict together with the raw provider evidence:
NONCE="$(openssl rand -hex 32)"
curl --get "$ANONROUTER_BASE_URL/tee/attestation" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
--data-urlencode "model=qwen/qwen3-32b" \
--data-urlencode "provider=chutes" \
--data-urlencode "nonce=$NONCE"The response binds model, provider, route_id, privacy_class, the
normalized attestation result, and the raw evidence. Require at least:
attestation.status == "ok"
attestation.privacy_modality == "e2ee"
attestation.supports_client_opaque_e2ee == true
attestation.nonce == the nonce your client generated
attestation.verification_level is accepted by your policy
every required attestation.checks item passedThe normalized result is AnonRouter's verification verdict. A client claiming
independent verification must also run the provider and silicon-vendor checks
over evidence; merely reading the normalized fields is not independent
verification. See TEE verification for the checks and
limits of each level.
NEAR v2 encryption-key attestation
NEAR's direct E2EE key uses an Ed25519 attestation flow. Obtain a single-use, model-bound attestation ticket from the authenticated control endpoint:
ATTESTATION_TICKET="$(curl -fsS \
"$ANONROUTER_BASE_URL/inference/attestation-tickets" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"openai/gpt-oss-120b","provider":"near-ai"}' \
| jq -r .ticket)"
NONCE="$(openssl rand -hex 32)"
curl -fsS "$ANONROUTER_BASE_URL/tee/attestation" \
-H "X-AnonRouter-Ticket: $ATTESTATION_TICKET" \
-H "Content-Type: application/json" \
-d "{\"nonce\":\"$NONCE\"}"This relay returns { "evidence": ... } without interpreting it. Independently
verify NEAR's TDX and NVIDIA evidence, the direct endpoint, measurements, nonce,
and the Ed25519 encryption-key commitment before using the model key. The
ticket is short-lived, single-use, and valid only for the selected direct
route. The two currently reviewed NEAR E2EE routes are
openai/gpt-oss-120b and z-ai/glm-5.2.
Do not substitute X-Model-Pub-Key from an unverified response. That header is
for NEAR's cloud gateway and is deliberately rejected on AnonRouter's direct
path.
3. Mint the inference ticket
After attestation passes, mint a separate inference ticket:
INFERENCE_TICKET="$(curl -fsS \
"$ANONROUTER_BASE_URL/inference/tickets" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-oss-120b",
"provider": "near-ai",
"e2ee": true,
"operation": "chat",
"max_tokens": 256
}' | jq -r .ticket)"The ticket and encrypted request must name the same model, provider, operation, reasoning controls, and output limit. A ticket expires quickly and can be redeemed only once.
For Chutes whole-body E2EE, set max_tokens to the route's complete published
maximum. Token usage is itself encrypted, so AnonRouter reserves and captures
the full conservative context ceiling rather than estimating plaintext from
ciphertext length.
NEAR AI version 2
NEAR encrypts each text field separately:
- Convert the attested model Ed25519 public key to X25519.
- Generate a fresh ephemeral X25519 key for every encrypted field.
- Derive the shared secret with X25519 ECDH.
- Derive 32 bytes with HKDF-SHA256, no salt, and
info = "ed25519_encryption". - Encrypt UTF-8 plaintext with XChaCha20-Poly1305 and a fresh 24-byte nonce.
- Lowercase-hex encode this wire value:
[ephemeral X25519 public key: 32 bytes]
[XChaCha20-Poly1305 nonce: 24 bytes]
[ciphertext and 16-byte authentication tag]Generate an Ed25519 client keypair and retain the private key only in your client. Send the public key and encrypted message fields:
curl -N "$ANONROUTER_BASE_URL/chat/completions" \
-H "X-AnonRouter-Ticket: $INFERENCE_TICKET" \
-H "X-AnonRouter-E2EE-Provider: near-ai" \
-H "X-Signing-Algo: ed25519" \
-H "X-Client-Pub-Key: $CLIENT_ED25519_PUBLIC_KEY_HEX" \
-H "X-Encryption-Version: 2" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"openai/gpt-oss-120b\",
\"messages\": [{\"role\":\"user\",\"content\":\"$ENCRYPTED_FIELD_HEX\"}],
\"max_tokens\": 256,
\"stream\": true
}"Decrypt encrypted content or reasoning_content values in the SSE response
with the retained client Ed25519 private key, converted to X25519. Reject a
field that is too short, malformed, or fails Poly1305 authentication.
AnonRouter validates only the fixed header vocabulary and ciphertext shape. It does not receive a private key or parse the encrypted message as plaintext. The direct enclave domain must exactly match the reviewed route; a missing or mismatched discovery result fails closed.
See NEAR's E2EE chat-completions guide and model verification guide for the independent client verifier.
Chutes ML-KEM whole-body transport
The verified Chutes evidence contains e2e_instances. Select an instance only
after verifying its UUID, ML-KEM-768 public key, short-lived nonce set, and
SHA256(caller nonce || ML-KEM public key) commitment in TDX report data.
Build the request with Chutes' transport:
- Generate an ML-KEM-768 response keypair.
- Add its base64 public key as
e2e_response_pkinside the JSON request. - Gzip the JSON.
- Encapsulate to the attested instance's ML-KEM-768 public key.
- Derive the request key with HKDF-SHA256. The salt is the first 16 bytes of
the ML-KEM ciphertext and
info = "e2e-req-v1". - Encrypt with ChaCha20-Poly1305 using a fresh 12-byte nonce.
- Concatenate
[ML-KEM ciphertext: 1088][nonce: 12][ciphertext + tag].
The encrypted inner request uses the provider model identifier, such as
Qwen/Qwen3-32B-TEE; the outer ticket stays bound to the canonical
qwen/qwen3-32b route. Relay the bytes without JSON or base64 wrapping:
curl "$ANONROUTER_BASE_URL/e2ee/chat/completions" \
-H "Content-Type: application/octet-stream" \
-H "X-AnonRouter-Ticket: $INFERENCE_TICKET" \
-H "X-AnonRouter-E2EE-Provider: chutes" \
-H "X-Chutes-Instance-Id: $ATTESTED_INSTANCE_ID" \
-H "X-Chutes-E2E-Nonce: $SINGLE_USE_NONCE" \
--data-binary @encrypted-request.bin \
--output encrypted-response.binDecapsulate the first 1088 response bytes with the retained response secret
key. Derive the response key with the first 16 ciphertext bytes as salt and
info = "e2e-resp-v1", authenticate/decrypt with ChaCha20-Poly1305, then
gunzip and parse the JSON locally.
The endpoint is non-streaming. Use each Chutes nonce once and retry only after fetching fresh evidence. The official Chutes E2EE transport repository is the reference implementation for framing and crypto.
Venice field encryption
Fetch public verified attestation for the exact Venice E2EE model and use its
attestation.attested_encryption_key only after every required check passes.
For each field:
- Generate an ephemeral secp256k1 keypair.
- Derive the ECDH shared secret with the attested model public key.
- Derive 32 bytes with HKDF-SHA256 and
info = "ecdsa_encryption". - Encrypt with AES-256-GCM and a fresh 12-byte nonce.
- Hex encode
[uncompressed ephemeral public key: 65][nonce: 12][ciphertext + tag].
Send a streaming request with the exact Venice header vocabulary:
curl -N "$ANONROUTER_BASE_URL/chat/completions" \
-H "X-AnonRouter-Ticket: $INFERENCE_TICKET" \
-H "X-AnonRouter-E2EE-Provider: venice" \
-H "X-Venice-TEE-Client-Pub-Key: $CLIENT_PUBLIC_KEY_HEX" \
-H "X-Venice-TEE-Model-Pub-Key: $ATTESTED_MODEL_PUBLIC_KEY_HEX" \
-H "X-Venice-TEE-Signing-Algo: ecdsa" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"venice/e2ee-gpt-oss-20b-p\",
\"messages\": [{\"role\":\"user\",\"content\":\"$ENCRYPTED_FIELD_HEX\"}],
\"max_tokens\": 256,
\"stream\": true
}"Decrypt response fields locally with your client private key. Venice requires streaming for its E2EE transport. Follow Venice's TEE and E2EE model guide for provider-side encryption and attestation details.
After a completed Venice stream, you can retrieve its enclave receipt through the public TEE signature service. The receipt proves the provider's signed enclave events; it does not claim that Venice signed AnonRouter's exact public wire bytes.
What AnonRouter can and cannot see
| Data | Control plane | Relay | Provider worker | Verified enclave |
|---|---|---|---|---|
| Account/API key | Yes | No | No | No |
| Single-use ticket | Issues it | Redeems it | Dispatch fence only | No |
| E2EE ciphertext | No | Yes, opaque | Yes, opaque | Decrypts it |
| Plaintext prompt/response | No | No | No | Yes |
| Provider credential | No | No | Yes | Provider-specific |
Applications should avoid logging ciphertext too. Although encrypted, it can remain sensitive if a client key is later compromised. Raw prompts, responses, private keys, and decrypted output should never be added to attestation caches or telemetry.
Launch limits and failure behavior
- Field E2EE accepts text
userandsystemmessages, requires a positive explicit output limit, and requires streaming. Tool calls, tool messages, images, and plaintext message fields are rejected. - Chutes whole-body E2EE is non-streaming and intended for text chat. The relay never parses the encrypted JSON to infer capabilities or token usage.
- Automatic routing and transparent provider fallback are disabled. A retry requires fresh attestation and a fresh inference ticket.
- Reusing an attestation ticket, inference ticket, Chutes nonce, or ephemeral field key is unsafe and rejected where the protocol exposes that state.
tee_signature_not_supportedmeans the provider has no compatible request receipt; it does not mean attestation or encrypted inference failed.provider-attestedis an honest current ceiling, not an alias forhardware-verified.
Production checklist
- Pin reviewed provider measurements, endpoint identities, release digests, and verifier versions in your client policy.
- Use a cryptographically secure random generator for every nonce and key.
- Compare the attested route and model to the ticket before encryption.
- Reject expired evidence, debug-enabled evidence, a failed required check, an unexpected verification level, or a changed encryption key.
- Keep API keys on your trusted control-side backend and private keys on the encrypting client.
- Treat AEAD authentication failure as a terminal security error; do not return partial plaintext.
- Never retry through a weaker privacy class.
For the exact attestation checks, measurement policies, request receipts, and provider-by-provider limitations, continue with TEE verification and encrypted inference.
How privacy works
The privacy model behind AnonRouter. Single-use tickets, separated roles, and why no single component can link who you are to what you send.
TEE verification and encrypted inference
Verify enclave evidence and per-request receipts, understand verification levels, and use opaque NEAR or Chutes E2EE.