API
API reference
What comes back that OpenAI does not send
"untold": {
"privacy_mode": … // What this answer was ACTUALLY delivered under, not what was requested.
"privacy_label": … // The same fact in the words shown on the answer itself.
"content_policy": … // Uncensored is what a paid plan buys. Privacy is not.
"receipt_digest": … // sha256 of the canonicalised receipt body — what the signature is over.
"receipt": … // Signed, and verifiable offline without us.
"commitment_salts": … // Returned to YOU and never stored by us. These are the inputs that let you recompute `promptCommit` and `responseCommit` from your own prompt and the answer — which is how a receipt is checked against reality rather than merely against itself.
"enclave_receipt_id": … // optional — present only when the gateway issued an ACI receipt.
"resolved_via_trait": … // optional — present only when a trait name resolved the model.
"why": … // optional — present only when the answer was delivered anonymized or private.
}The salts are how a receipt commits to your prompt without containing it. They are sent once, to you, and we keep no copy — so we cannot open the commitment either.
End-to-end encryption
The strongest mode, and available to you on the same terms as the web app — your key, your device. Fetch the enclave's attested public key, encrypt the message content to it, and post the ciphertext:
GET /api/e2ee/key?model=untold-uncensored # -> algo, public_key, upstream_model, measurement
POST /api/e2ee/relay # the same body, content fields encrypted
headers: X-E2EE-Version: 2 · X-Client-Pub-Key · X-Model-Pub-Key
X-E2EE-Nonce (32 random bytes, hex) · X-E2EE-Timestamp (unix seconds)Encrypt under upstream_model, not the id you asked for. The associated data binds the request's model string byte-exactly, and we forward the upstream name — that is why the key endpoint tells you what it is rather than leaving you to guess and fail at the last step.
We cannot finish the proof for you, and that is the point. The enclave's receipt commits to the DECRYPTED request, which we never hold. We verify every other half — the Intel chain, the pinned measurement, the workload identity, the receipt's binding to that quote, that the upstream model was itself attested, and its commitment to this exact response — then hand you the receipt. Hash your own request body and compare it to the request.received event to complete it.
On this path we cannot moderate what you send and do not store the exchange. That is not a policy we are choosing; it is a consequence of not being able to read it.
Refusals are the product working
503 attestation_unavailable— no enclave could be verified right now.503 unverified_enclave— an answer came back without provable provenance.409 shown_mode_stale— you sentshown_modeand we would have delivered something weaker.403 not_entitled— that model needs a paid plan.429— rate limited.
A refusal never carries an answer. If you get content, it was proved.
Other endpoints
GET /v1/models— the catalogue with each model's mode (26 models, 9 sealable).GET /v1/models/traits— stable names that outlive a model.GET /api/limits— what we do not promise, computed from the running system.GET /api/metrics— the numbers that would catch us lying.
This is what you get back
A real receipt, from a sealed answer this system produced on . The signature is EIP-191 over RFC 8785 canonical JSON, so ordinary Ethereum tooling reads it and no call back to us is needed.
{
"body": {
"v": 1,
"issuedAt": "2026-08-26T08:38:43Z",
"model": "untold-uncensored",
"mode": "tee",
"promptCommit": "7c8f92a4df1df84a7de62ecc29bad7fbafff651f49e55334747ff7b3806022c3",
"responseCommit": "9b6709e2d77f23a5317fbdefe25939b137e6b7140fd4a0517b9b9262134608e4",
"enclave": {
"providerId": "phala-aci-v1",
"measurement": "bd369a8c2f9edb2b52dad48ac8e0b32dde5f1337c423a506b48d07403a7d8033",
"verifiedBy": "intel-rooted-v1"
}
},
"digest": "01429c567285cd096ad51512e8e0b1c661ca5ba0736b0b13ac657cd12e36091e",
"signature": "0x19f5aff315109319f55339fd9b75f10ba854ca516e29a935ae5f618b9137fb2e7fd11cb144cc1ab119ad591a45c8d79f3ac0fa6f5446b5ce810a1da7b1fb74611c",
"signer": "0x95A66186d7D2b5856491370e5F5182D1F10DA085"
}Or check a different one — yours, or one you were sent — at /verify.
No network call was made to check it.
Every field here is a commitment or a measurement. The prompt and the answer are not in it — only hashes of them, which is why this one can sit on a public page. Opening a commitment needs the salts, and those go to the person who asked, not into this file. The address that signed it is published at /limits, and checking against it is what separates “internally consistent” from “actually from Untold”.
Verifying a receipt
Our signing address is 0xA1641C53Be06C7d3370403e7daC01660A209F8D1, also at /api/limits as seal_signer. Fetch it from there rather than from a receipt: a signature checked against an address that arrived in the same file shows only that whoever wrote the file also signed it.
The verifier itself is not published yet — npx @untold/verify does not resolve and this repository is private. Until it is, the signature is EIP-191 over the sha256 digest of the canonicalized body — that is, compute digest = sha256(JCS(body)) and verify the EIP-191 signature over that 64-character hex string, not over the canonical bytes themselves. Ordinary Ethereum tooling checks it once you sign the right message.
Opening a commitment
A receipt carries a salted commitment to your prompt rather than a hash of it, so nobody holding a filed receipt can confirm a guess. The salts come back to you in salts and are never stored, which is also why only you can do this:
promptCommit = sha256( salts.prompt + ":" + JCS(request.messages) )
responseCommit = sha256( salts.response + ":" + JCS(response.choices[0].message) )JCS is RFC 8785 canonical JSON, the same canonicalisation the signature uses, and the separator is a literal colon. Recompute either value with your saved salt and the exact JSON you sent; if it matches the receipt, you have proved what you asked without revealing it to anyone.