Skip to content

Troubleshooting

Organized by what you see, not by status code. For the complete status, type, and code matrix, see Error codes.

Every error response carries request_id at the top level, next to the error object. Log it before you do anything else; it is the only handle anyone has on a specific request.

Symptom. Nothing works, whatever the model or payload. The status is 401.

Likely cause. Read error.code, because the three cases have different fixes.

  • missing_authorization — no Authorization header reached the Router. Usually an unset environment variable, which sends the literal string Bearer and reads as absent, or a proxy stripping the header.
  • invalid_api_key — the key is malformed or was never issued. Most often a truncated copy or a stray newline.
  • A revoked key returns this same invalid_api_key code rather than a distinct one. Revocation is immediate, so a key that worked minutes ago starts failing with no warning and no way to tell revocation from a typo except your own records.

How to confirm. Print the length of the variable in the process that actually makes the call, not in your shell. Then check the key still exists in the Console under Dashboard → API.

Fix. Correct the credential. Nothing about a 401 is transient, so do not retry it. If the key was revoked, deploy the replacement everywhere — see Create and manage API keys.

Symptom. Requests that worked yesterday return 402 with type: payment_error. The key is fine.

Likely cause. The deposit no longer covers the request. Balance is drawn down per request, so a long batch job can cross zero mid-run.

How to confirm. Read the balance with a management key:

Terminal window
curl https://router-api.0g.ai/v1/account/balance \
--fail-with-body \
--max-time 30 \
-H "Authorization: Bearer $ZG_MANAGEMENT_KEY"

Fix. Deposit more 0G — see Get 0G and fund your account. Retrying cannot create funds. Two things to keep in mind while sizing the top-up: deposits are one-way and cannot be withdrawn, and you want a buffer rather than a top-up every few hours, so alert on runway before zero rather than reacting to the 402.

403 insufficient_scope, and the key is definitely valid

Section titled “403 insufficient_scope, and the key is definitely valid”

Symptom. Chat completions work with the key, but /v1/account/balance, /v1/account/funds, or /v1/account/usage/stats returns 403.

Likely cause. You are calling an account endpoint with a sk- inference key. sk- keys have no scope concept and are rejected from /v1/account/* unconditionally. This is deliberate, not a misconfiguration, and it is the most common source of the “but my key works” confusion.

How to confirm. Look at the prefix of the credential the failing call sends. If it starts with sk-, that is the answer.

Fix. Use an mk- management key carrying account:read, issued from a wallet session in the Console. Keep the two in separate variables — ZG_API_KEY and ZG_MANAGEMENT_KEY — so the wrong one cannot be picked up by accident. If you are already sending an mk- key and still get insufficient_scope, the key exists but lacks that scope; re-issue it. See Create and manage API keys.

Symptom. Bursts start failing with 429, and throughput collapses further if you retry hard.

Likely cause. You crossed your account’s request limit.

How to confirm. Every successful inference response carries X-RateLimit-Limit-Requests, X-RateLimit-Remaining-Requests, and X-RateLimit-Reset-Requests. Read your real position from those rather than guessing at a number.

Fix. Honor Retry-After on the 429 and sleep for exactly that long. Better, back off before you get there, when X-RateLimit-Remaining-Requests approaches zero, and queue rather than spray. Check that you are not stacking your own retry loop on top of your SDK’s.

Symptom. 503 with a message naming a tier, such as no provider available for trust mode: tier=private. Only requests carrying a trust mode fail.

Likely cause. Capacity exists for the model, but not in the tier you asked for. The Router never silently serves you from a weaker tier — it fails instead. That is the guarantee working, not breaking.

How to confirm. List the models with a provider in the strictest tier:

Terminal window
curl -s https://router-api.0g.ai/v1/models \
| jq '.data[] | select(.verifiability == "TeeML") | .name'

If your code sends no trust mode header at all, check the key: a trust mode set on the key applies to every request it makes and cannot be overridden by a header.

Fix. Retry slowly — tier supply fluctuates — or move to a model that has a provider at your tier. Trust mode is a floor, so a verified request is also satisfied by a private provider; private is the narrowest constraint and the most likely to be unsatisfiable.

503 no_available_provider, or 502 provider_error

Section titled “503 no_available_provider, or 502 provider_error”

Symptom. A 5xx from the Router and no obvious way to tell whether retrying is worth it.

Likely cause. They describe opposite situations.

502 provider_error 503 no_available_provider
What happened A provider was reachable and returned an error No healthy provider existed to try
Failover Already exhausted; every healthy candidate was tried Never started; the candidate set was empty
Retry value Worth a short retry, a provider may have just recovered Unlikely to clear in seconds

How to confirm. Check current supply for the model:

Terminal window
curl -s "https://router-api.0g.ai/v1/providers?model=<MODEL_ID>"

Fix. For 502, retry a couple of times with bounded exponential backoff and jitter; if it persists, the model’s providers are broadly unhealthy and switching models is faster than waiting. For 503, do not hammer — switch model or wait. If you pinned an address with X-0G-Provider-Address, note that pinning implies Allow-Fallbacks: false, so a single sick provider means no fallback at all. Drop the pin or send X-0G-Provider-Allow-Fallbacks: true. See Provider routing headers.

Adding a routing header turned a working request into a 400

Section titled “Adding a routing header turned a working request into a 400”

Symptom. The request succeeded until you added an X-0G-Provider-* header. Now it returns 400 invalid_provider_header, 400 invalid_trust_mode, or 400 invalid_max_price_usd.

Likely cause. The header is present but its value is outside the allowed set. A missing or blank header is treated as unset and never errors, so a 400 means something in your stack really is emitting a bad value.

Header Accepts Common mistake
X-0G-Provider-Sort latency, price, exactly and lowercase cost, cheapest, fastest
X-0G-Provider-Allow-Fallbacks true, false, case-insensitive 1, 0, yes, on
X-0G-Provider-Trust-Mode standard, verified, private sealed, tee, a value with trailing whitespace
X-0G-Provider-Max-Price-Usd-* A finite, non-negative decimal $0.50, 0.5 USD, -1

How to confirm. Log the exact string your client sends, including quoting and whitespace. Values interpolated from config often arrive with quotes still attached.

Fix. Match the value domain exactly. Header names are case-insensitive; values mostly are not. Full semantics: Provider routing headers.

400 no_provider_within_max_price, and retrying never helps

Section titled “400 no_provider_within_max_price, and retrying never helps”

Symptom. A price ceiling that used to work now fails every time, immediately.

Likely cause. The ceiling filtered out every candidate provider. This is structural, not transient — the providers are healthy, they are simply priced above your limit, which is why it is a 400 and not a 503. The related pinned_provider_exceeds_max_price means the specific provider you pinned is over the ceiling.

How to confirm. Look at what providers currently charge for the model with GET /v1/providers?model=<MODEL_ID>.

Fix. Raise the ceiling, drop the pin, or choose a cheaper model. Do not retry unchanged; the ceiling is a hard filter that runs before sorting and before failover, so waiting changes nothing. See Cap what a single request may cost.

Symptom. A streaming response ends mid-sentence with no error frame, or the client raises a read timeout.

Likely cause. Usually a client-side or intermediary read timeout rather than a Router failure. Reasoning-heavy generations can go a long time before the first token, and a default HTTP timeout fires in that gap. A reverse proxy, load balancer, or serverless platform in between can impose its own idle limit that never shows up in local testing.

How to confirm. Check whether the last chunk carried a finish_reason. Without one, the answer is partial no matter how complete the text looks.

Fix. Set an explicit read timeout well past your worst-case generation instead of relying on defaults, and check every hop between you and the Router for its own limit. Then decide what a partial means for your application: there is no resume, and a retry re-runs the whole completion and is billed again. Discard partials for structured output; mark them as truncated for prose. See Stream responses.

The request succeeded but tee_verified is false

Section titled “The request succeeded but tee_verified is false”

Symptom. The response arrived normally and x_0g_trace.tee_verified is false.

Likely cause. A signature was present and failed verification. That is not a warning — treat the response as untrusted and do not use its content. A missing tee_verified field means something different: verification was never requested.

How to confirm. Verify independently rather than trusting the Router’s flag. Take the chat ID from the ZG-Res-Key response header, fetch the provider’s signature for it, and check that it signs the content you received.

Fix. Discard the response and re-run the request. If it repeats on the same provider, pin away from it or raise the trust tier. Full procedure: Verify a response.

The model does not exist, or has no TEE provider

Section titled “The model does not exist, or has no TEE provider”

Symptom. A model name that works in the Console fails through the API, or works normally until you add a trust mode and then returns 503 no_provider_for_trust_mode.

Likely cause. Either the identifier is wrong, or the model has no provider in the tier you need. Not every model runs on TeeML.

How to confirm. The catalog is public and needs no authentication:

Terminal window
curl -s https://router-api.0g.ai/v1/models | jq '.data[] | {name, verifiability}'

Fix. Use the identifier exactly as the catalog returns it, and pick a model whose verifiability supports the tier you plan to request. See Model catalog.

Collect the request_id from the failing response. It is present on every error, alongside the error object, and on success inside x_0g_trace. Because prompts and completions are never stored, that ID plus the timestamp and error.code is what makes a report actionable — a description of the prompt is not.