Verification and E2EE
Verify both hops of a confidential route with one verdict you can gate on, and run end-to-end-encrypted inference, with @anonrouter/confidential (JS/TS) and anonrouter-confidential (Python).
@anonrouter/confidential (JavaScript / TypeScript) and anonrouter-confidential
(Python) are twin packages that let your own app do three things without trusting
AnonRouter: verify a route's evidence, encrypt inference requests end to
end, and generate images and speech across the two-origin split.
The verifier is pure over its inputs, fails closed, and both languages pass the same shared known-answer test vectors, so they agree bit-for-bit.
What TEE and E2EE each buy you
Your plaintext does not reach ordinary AnonRouter infrastructure. All content
goes to api.anonrouter.ai, whose relay runs inside an attested Intel
TDX CVM and terminates TLS in-enclave — transport_terminates_in_tee and
tls_certificate_bound_to_quote are required checks in the shipped policy, so
the attested TD provably holds the key for your connection.
What still differs between the modalities is the trust set. On a TEE
route the relay handles your plaintext inside that enclave, so you are trusting
our reviewed build — a build changed to exfiltrate it would change the
measurements and hop 1 would stop verifying, making cheating detectable rather
than impossible. On an E2EE route (NEAR, Venice, Chutes) the request is
encrypted to the provider's attested key, so the relay holds ciphertext
whatever code it runs and our build is not in your trust set at all. On some
E2EE routes the enclave holding that key is a verified provider gateway rather
than the model host; the catalog's per-route privacyNotes say which.
Every verdict reports which one you got as contentVisibleToAnonRouter, rather
than leaving you to infer it from the provider name.
Install
The registry commands below do not work yet
Neither package is published on npm or PyPI. Both registry names return 404
today. Until they are published, take the tarball, wheel, or sdist from the
v0.1.0 release,
check it against the release SHA256SUMS, and install the file:
npm install ./anonrouter-confidential-0.1.0.tgz
pip install ./anonrouter_confidential-0.1.0-py3-none-any.whl# Not published yet — see the callout above.
npm install @anonrouter/confidentialNode 22+ or Python 3.10+. You need an API key with the inference scope.
Configuration: two origins
AnonRouter serves two browser-visible origins with different jobs, and the SDK keeps them apart for you.
| Origin | Role | What it receives |
|---|---|---|
https://control.anonrouter.ai | control | your API key, content-free ticket and catalog requests |
https://api.anonrouter.ai | confidential | attestation evidence and request content, authenticated by a single-use ticket alone |
Both are the SDK's defaults, so most callers configure nothing but a key.
import { createClient } from "@anonrouter/confidential";
// Production origins are the defaults.
const client = createClient({ apiKey: process.env.ANONROUTER_API_KEY! });
// Or name them explicitly, for another deployment:
const explicit = createClient({
inferenceBaseUrl: "https://api.anonrouter.ai",
controlBaseUrl: "https://control.anonrouter.ai",
apiKey: process.env.ANONROUTER_API_KEY!,
});Why the origin matters to verification
A gateway quote binds the exact origin the client connected to. Verifying one origin while sending content to another would prove nothing about the route that carried your prompt, so the SDK deliberately offers no way to do it: evidence and content always use the same confidential origin. The control origin only ever carries the API key and content-free metadata.
The two hops
A request travels through two parties, and verifying one tells you nothing about the other.
| Question it answers | Endpoint | |
|---|---|---|
| Hop 1 — AnonRouter's own routing plane | Is the data plane I am connected to the exact reviewed build, running inside an Intel TDX confidential VM, bound to my nonce and my origin? | GET /v1/gateway/attestation |
| Hop 2 — the downstream provider route | Did the model provider terminate my request inside a verified enclave running measurements I pinned? | /v1/tee/attestation |
A verified hop 2 says nothing about who routed the request. A verified hop 1 says nothing about where inference actually ran. Only asking both establishes the whole path.
verifyRoute() — the stable contract
verifyRoute establishes both hops, cross-binds them to the route you asked
for, and reports ordered states rather than a boolean. This is the call to build
on.
import { createClient, atLeast } from "@anonrouter/confidential";
const client = createClient({ apiKey: process.env.ANONROUTER_API_KEY! });
const verdict = await client.verifyRoute({
model: "venice-uncensored",
provider: "venice",
gateway: true, // omit to skip hop 1 entirely
});
verdict.overallState; // the weakest hop you ASKED about
verdict.gateway.requested; // whether hop 1 was in scope at all
verdict.gateway.failedChecks; // the exact required checks that failed
verdict.bindingMismatches; // route asked for vs route attested
verdict.contentVisibleToAnonRouter; // true on a tee route
if (!atLeast(verdict.overallState, "cryptographically_checked")) {
throw new Error(`route not established: ${verdict.reason}`);
}Cross-binding is what this adds
Each hop's verifier only ever sees its own evidence, so neither can notice that
the gateway attested itself perfectly while serving a different provider, model,
or privacy class than you asked for. Any disagreement lands in
bindingMismatches and forces untrusted, however strong the individual hops
were. Two honestly-attested parties on the wrong route is still the wrong route.
The five states
Strictly ordered. Gate with atLeast() / at_least() rather than comparing
strings, so a threshold keeps meaning the same thing if a state is later inserted
into the scale.
| State | What it establishes |
|---|---|
hardware_verified | Everything below, plus the quote's signature chained to the vendor's roots with an accepted TCB status. Reachable on hop 1 with a DCAP engine. |
cryptographically_checked | The evidence is internally consistent and bound to your fresh nonce, and the measurements match local pins. Nothing proved it came from real silicon. |
policy_matched | The evidence's claimed identity matched local pins, established through a vendor SDK rather than by our own parse. |
untrusted | We looked and it failed. |
unavailable | We could not look. The deployment does not expose the endpoint. |
`untrusted` and `unavailable` are both failures, and they are different
"We looked and it failed" and "we could not look" call for different responses. Collapsing them into one boolean is how a missing endpoint gets mistaken for a passing check.
Reaching hardware_verified
Neither package bundles a DCAP engine. Shipping prebuilt binaries would mean
asserting that a binary we did not build reproducibly is the reviewed one, and a
hand-rolled reimplementation would be an unreviewed version of the single
component whose failure mode is printing hardware_verified for a forged quote.
What ships instead is a strict adapter to the reviewed engine, plus the
Intel-signed collateral it needs (the engine performs no network access, on
purpose). Install anonrouter-dcap-verifier, put it on PATH or name it in
ANONROUTER_DCAP_VERIFIER_BIN, and hop 1 can reach hardware_verified.
import { createAnonRouterDcapVerifier, describeDcapInstallation } from "@anonrouter/confidential/dcap";
describeDcapInstallation(); // is one installed? which one? what is its digest?
const verdict = await client.verifyRoute({
model, provider,
gateway: { chainVerifier: createAnonRouterDcapVerifier() },
});The adapter fails closed on every path: a missing engine, a digest that does not
match the expected one, a timeout, a crash, non-JSON output, collateral it could
not acquire, or a verdict whose measured report disagrees with the quote the SDK
parsed. Without an engine the ceiling is cryptographically_checked, and a policy
demanding hardware verification fails closed. It never silently downgrades.
The pins do not come from the server being checked
A server that could hand you the list of builds you accept could always name itself. The measurement pins therefore ship inside the package, and an origin with no pin fails rather than falling back to whatever the server claims.
End-to-end-encrypted chat
For an E2EE provider the SDK encrypts your prompt in your process, relays only ciphertext, and decrypts the reply locally. It verifies the enclave first and binds the encryption key to that attestation, so it will not encrypt to an enclave it could not verify.
const reply = await client.chat({
model: "venice-uncensored",
provider: "venice",
messages: [{ role: "user", content: "Draft a private message." }],
maxOutputTokens: 512,
// Re-establish hop 1 with a NEW nonce before a ticket is spent: a verdict from
// a minute ago is a fact about a minute ago.
requireGateway: true,
});
console.log(reply.content);
console.log(reply.usage);requireGateway / require_gateway runs the check before the first
authenticated call, so a failure means no ticket was spent and no plaintext went
near the wire.
Conversations are multi-turn and text-only. Because the enclave is stateless you re-send the conversation each turn and the SDK re-encrypts it fresh, exactly like any stateless chat API. Tool calls, images, and attachments are not part of the encrypted surface and fail closed.
Providers and ceilings
| Provider | Modality | Hop 2 ceiling |
|---|---|---|
venice | E2EE (secp256k1 ECDH, AES-256-GCM) | cryptographically_checked |
chutes | E2EE (ML-KEM-768, ChaCha20-Poly1305) | cryptographically_checked |
tinfoil | TEE — our attested relay handles plaintext in-enclave | policy_matched |
near-ai | E2EE (Ed25519 / X25519, XChaCha20-Poly1305) | cryptographically_checked |
Chutes is the only post-quantum route. Tinfoil is TEE-only and reaches
policy_matched through its official verifier (the optional tinfoil
dependency); without it, Tinfoil verification fails closed.
The near-ai verifier is registered but the live catalog publishes no callable
NEAR route, so nothing selects that provider today. The callable confidential
set is Tinfoil, Venice, and Chutes.
A Venice E2EE route may decrypt at a gateway, not at the model host
contentVisibleToAnonRouter answers the question the SDK can answer: whether
our build sees plaintext. It does not describe the provider's internal
topology. On some Venice routes the enclave your client encrypts to is a
verified gateway that decrypts and forwards onward inside Venice. The route's
privacyNotes in the catalog state this per route.
Hop 2 does not reach hardware_verified on any provider: several of these routes
run GPU enclaves whose NVIDIA attestation chain is not available to verify, and
chaining only the CPU quote would claim more than was checked.
Chutes E2EE bills a flat, full-ceiling reservation
Chutes' protocol encrypts the whole request body, so the gateway cannot meter it
token by token. It therefore reserves and bills the model's full context window
plus full output ceiling, flat, regardless of how short your prompt or
maxOutputTokens is. On a large-context model that is significant. NEAR and
Venice are metered from ciphertext length and cost the usual per-token amount.
For a cheap encrypted round-trip prefer a small-context Chutes model or a
NEAR / Venice route — and remember verification itself is always free.
The security contract
- Independent re-verification. The SDK parses the raw TDX quote and provider
evidence itself and checks it against pins shipped inside the package. It never
turns a provider's
verified: true, a model suffix, or the gateway's verdict into a proof. - Fresh nonce, key binding. Every verification uses a fresh nonce and confirms the enclave bound its key to that nonce, so evidence cannot be replayed.
- Credential isolation. Your API key authenticates only content-free control requests. It is never sent to the origin that carries content.
- No escape hatch. There is no environment variable that weakens a verdict.
The suites set a dozen plausible spellings of one (
ANONROUTER_INSECURE,ANONROUTER_SKIP_VERIFY,NODE_ENV=test,CI=true, and so on) and require the verdict for the same evidence to come out identical, not merely also-failing.
Errors
Failures raise a typed ConfidentialError with a stable code — such as
attestation_untrusted, nonce_mismatch, or inference_ticket_failed — and a
content-free message. Error messages never carry prompt or key material by
construction. Media errors subclass the same type.
Advanced use
Both packages export the building blocks, for callers who want to verify or decrypt without the high-level client:
- Pure verifiers per provider (
NearTeeVerifier,VeniceTeeVerifier,ChutesTeeVerifier,TinfoilTeeVerifier) plusverifyRawEvidence. - TDX quote parsing:
parseTdxQuote,matchMeasurementAllowlist. - Reviewed pins:
pinnedMeasurementPolicyFor,pinnedGatewayPolicyFor. - Low-level provider crypto:
providerCrypto["near-ai" | "venice" | "chutes"](JS) oranonrouter_confidential.crypto.{near,venice,chutes}(Python), exercised by the shared known-answer vectors.
Release artifacts
v0.1.0 is
the first public release: implementation, reviewed measurement pins, gateway
policies, and the shared known-answer vectors ship as one auditable set under
Apache-2.0, with the DCAP verifier's source under AGPL-3.0-only in
native/dcap-verifier. SHA256SUMS covers both npm tarballs, the wheel, the
sdist, the reproducible linux/amd64 verifier binary, and both source archives.
linux/amd64 is the only target with a reproducible artifact and the only one
claimed.
Two things about it are worth knowing before you depend on it:
- The registry packages are not published. npm and PyPI both
404. Install from the release artifacts. - The shipped gateway pin predates the running content plane.
v0.1.0pins the release that was live when it was tagged, and the plane has been released since. Hop 1 therefore fails closed against production oncompose_hash_pinnedandrelease_pinnedrather than passing — which is the intended behaviour for a stale pin, but it means--require hardware_verifiedneeds a--policyyou reviewed yourself until a refreshed pin ships.
SDKs
The AnonRouter SDKs for verifying confidential routes, running E2EE inference, and generating images and speech across the two-origin privacy split. Source and release artifacts are on GitHub; the registry packages are not published yet.
Images and speech
Generate images and synthesize speech across AnonRouter's two-origin privacy split with client.images.generate and client.audio.speech.create.