Skip to content

Verify a response came from the enclave

Every TEE-backed provider signs its response inside the enclave. That signature is what turns “the model I asked for answered me” from a promise into something you can check. This page covers the one-flag path, where the Router checks the signature for you, and the independent path, where you check it yourself and trust nothing but the chain and the provider.

Add verify_tee: true to the request body. It is a 0G extension: the Router strips it before forwarding, so it never reaches the model and never conflicts with the OpenAI-compatible schema.

Terminal window
curl https://router-api.0g.ai/v1/chat/completions \
--max-time 120 \
--fail-with-body \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ZG_API_KEY" \
-d '{
"model": "glm-5.2",
"messages": [{"role": "user", "content": "Hello"}],
"verify_tee": true
}'

Multipart endpoints have no JSON body, so pass it as a query parameter instead:

Terminal window
curl "https://router-api.0g.ai/v1/audio/transcriptions?verify_tee=true" \
--max-time 300 \
--fail-with-body \
-H "Authorization: Bearer $ZG_API_KEY" \
-F "file=@<YOUR_AUDIO_FILE>" \
-F "model=whisper-large-v3"

Every Router response carries an x_0g_trace block. With verify_tee set, it gains tee_verified.

{
"id": "chatcmpl-0852f405-6c56-40c2-a800-e6fd70785065",
"object": "chat.completion",
"model": "glm-5.2",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hello." },
"finish_reason": "stop"
}
],
"x_0g_trace": {
"request_id": "7f3c1e08-9a4b-4d21-8f6e-2b5c9d0a1e34",
"provider": "0x7DCFe6AEa70350C2090041524c9B4A9262DCe87D",
"billing": { "input_cost": "85600000000000", "output_cost": "285760000000000", "total_cost": "371360000000000" },
"tee_verified": true
}
}

Live response, 2026-07-20.

Read the field as three-valued, not as a boolean:

tee_verified Meaning What to do
true The provider’s TEE signature validated Accept the response
false A signature was present and did not verify Treat the response as untrusted
absent Verification was not requested for this response You omitted verify_tee, or an intermediary stripped it

The distinction matters. A check like if (trace.tee_verified) puts “you never asked” and “verification failed” into the same silent-pass branch.

When Router-side verification is not enough

Section titled “When Router-side verification is not enough”

verify_tee: true asks the Router to fetch the signature, look up the signer address on-chain, and check it. What comes back is one boolean summarizing that work. The raw signature is not returned to you, so tee_verified: true means the Router says it verified the signature.

For most applications that is the right trade. For an audit trail, compliance evidence, or any setting where the Router itself is inside your threat model, the flag is not evidence — reproduce the check. Everything it uses is public, so you can.

The check needs two inputs: the provider address from x_0g_trace.provider, and the chatID, which comes from the ZG-Res-Key response header. The body’s id is a fallback only if that header is absent. Because you need raw headers, call the endpoint with fetch rather than through an SDK object.

import { ethers } from "ethers";
import { createZGComputeNetworkBroker } from "@0gfoundation/0g-compute-ts-sdk";
// Any wallet works: the check only reads the chain and calls the provider's
// public signature endpoint. No funds, no signing.
const rpc = new ethers.JsonRpcProvider("https://evmrpc.0g.ai");
const wallet = ethers.Wallet.createRandom().connect(rpc);
const broker = await createZGComputeNetworkBroker(wallet);
// 1. Make the request with fetch so response headers stay readable.
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 120_000);
let response: Response;
try {
response = await fetch("https://router-api.0g.ai/v1/chat/completions", {
method: "POST",
signal: controller.signal,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ZG_API_KEY}`,
},
body: JSON.stringify({
model: "glm-5.2",
messages: [{ role: "user", content: "Hello" }],
}),
});
} finally {
clearTimeout(timer);
}
if (!response.ok) {
// 429 -> honour Retry-After; 502 -> short retry; 400/401/402 -> fix the request.
throw new Error(`router error ${response.status}: ${await response.text()}`);
}
const data = await response.json();
// 2. Pull the two inputs.
const providerAddress: string = data.x_0g_trace.provider;
const chatID: string = response.headers.get("ZG-Res-Key") ?? data.id;
// 3. Verify against the chain and the provider, not against the Router.
const isValid = await broker.inference.processResponse(providerAddress, chatID);
// true -> independently verified
// false -> verification failed; treat the response as untrusted
// null -> the provider exposes no verifiable TEE service; there is nothing to check
if (isValid !== true) {
throw new Error(`response not independently verified (result: ${String(isValid)})`);
}

The SDK package is @0gfoundation/0g-compute-ts-sdk.

null is not success. Decide explicitly whether “nothing to verify” is acceptable; if it is not, constrain routing to verified or private with X-0G-Provider-Trust-Mode so only providers with a verifiable service are eligible. See Trust modes and Headers.

From a language with no 0G SDK, reproduce the same four steps.

  1. Read the provider’s service record on-chain, keyed by the provider address from x_0g_trace. RPC endpoint: https://evmrpc.0g.ai. The record gives url, teeSignerAddress, and verifiability. If additionalInfo.targetSeparated is set, use additionalInfo.targetTeeAddress as the signer instead.
  2. GET {url}/v1/proxy/signature/{chatID}?model={model} — returns {text, signature}.
  3. Verify signature as an EIP-191 personal_sign over text against the signer address. Any standard Ethereum library does this.
  4. Compare the signed text against the response content the Router returned to you.

All four passing gives end-to-end proof with no trust in the Router. Step 4 is the one people skip: without it you have proven that a signature exists, not that it covers the bytes you were served.

Symptom Cause Fix
tee_verified absent verify_tee not sent, or dropped by an intermediary Send it in the JSON body, or as ?verify_tee=true on multipart endpoints
tee_verified: false Signature present but invalid Treat the response as untrusted; quote x_0g_trace.request_id when reporting it
processResponse returns null Provider exposes no verifiable TEE service Constrain routing to verified or private
ZG-Res-Key is missing Reading a parsed SDK object instead of raw headers Use fetch, or fall back to the body id
401 invalid_api_key ZG_API_KEY unset, wrong, or an mk- key Inference needs an sk- key
503 no_provider_for_trust_mode Routing constrained to a tier with no supply Back off, or switch to another verifiable model

Full list with retry guidance: Error codes.