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.
Prerequisites
Section titled “Prerequisites”- An account with a funded balance. See Get 0G and fund your account.
- An
sk-API key from the 0G Private Computer Console, exported asZG_API_KEY. - A model with a verifiable provider. The examples use
glm-5.2, which has a TeeML provider. Constrain routing with Trust modes if you need to guarantee one is selected.
Ask the Router to verify
Section titled “Ask the Router to verify”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.
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:
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"import OpenAI from "openai";
const apiKey = process.env.ZG_API_KEY;if (!apiKey) throw new Error("ZG_API_KEY is not set");
const client = new OpenAI({ apiKey, baseURL: "https://router-api.0g.ai/v1", timeout: 120_000, maxRetries: 0,});
type ZgTrace = { request_id: string; provider: string; billing: { input_cost: string; output_cost: string; total_cost: string }; tee_verified?: boolean;};
async function askVerified(prompt: string): Promise<string> { let res; try { res = await client.chat.completions.create({ model: "glm-5.2", messages: [{ role: "user", content: prompt }], // 0G extension; absent from the OpenAI types, stripped before forwarding. verify_tee: true, } as never); } catch (err) { if (err instanceof OpenAI.APIError) { throw new Error(`router error ${err.status} (${(err.error as { code?: string } | undefined)?.code})`); } throw err; }
const trace = (res as unknown as { x_0g_trace: ZgTrace }).x_0g_trace;
// Three-valued: true / false / absent. Never coerce to a boolean. if (trace.tee_verified === undefined) { throw new Error(`verification was not performed for ${trace.request_id}`); } if (trace.tee_verified === false) { throw new Error(`signature did not verify for ${trace.request_id}`); }
return (res as unknown as { choices: { message: { content: string } }[] }).choices[0].message.content;}import os
from openai import APIError, OpenAI
api_key = os.environ["ZG_API_KEY"]
client = OpenAI( api_key=api_key, base_url="https://router-api.0g.ai/v1", timeout=120.0, max_retries=0,)
def ask_verified(prompt: str) -> str: try: res = client.chat.completions.create( model="glm-5.2", messages=[{"role": "user", "content": prompt}], extra_body={"verify_tee": True}, # 0G extension, stripped before forwarding ) except APIError as err: raise RuntimeError(f"router error: {err}") from err
trace = (res.model_extra or {}).get("x_0g_trace", {})
# Three-valued: True / False / absent. Never coerce to a bool. if "tee_verified" not in trace: raise RuntimeError(f"verification was not performed for {trace.get('request_id')}") if trace["tee_verified"] is False: raise RuntimeError(f"signature did not verify for {trace['request_id']}")
return res.choices[0].message.content or ""Expected output
Section titled “Expected output”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.
Verify independently with the SDK
Section titled “Verify independently with the SDK”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.
Verify without the SDK
Section titled “Verify without the SDK”From a language with no 0G SDK, reproduce the same four steps.
- Read the provider’s service record on-chain, keyed by the
provideraddress fromx_0g_trace. RPC endpoint:https://evmrpc.0g.ai. The record givesurl,teeSignerAddress, andverifiability. IfadditionalInfo.targetSeparatedis set, useadditionalInfo.targetTeeAddressas the signer instead. GET {url}/v1/proxy/signature/{chatID}?model={model}— returns{text, signature}.- Verify
signatureas an EIP-191personal_signovertextagainst the signer address. Any standard Ethereum library does this. - Compare the signed
textagainst 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.
Common errors
Section titled “Common errors”| 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.
Next steps
Section titled “Next steps”- Harden for production — make verification a gate rather than a log line
- Verification and TEE — what the enclave attests to, and what each layer proves