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.
Every request fails with 401
Section titled “Every request fails with 401”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— noAuthorizationheader reached the Router. Usually an unset environment variable, which sends the literal stringBearerand 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_keycode 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.
402 insufficient_balance
Section titled “402 insufficient_balance”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:
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.
429 rate_limit_exceeded
Section titled “429 rate_limit_exceeded”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.
503 no_provider_for_trust_mode
Section titled “503 no_provider_for_trust_mode”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:
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:
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.
A stream stops partway through
Section titled “A stream stops partway through”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:
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.
Still stuck
Section titled “Still stuck”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.
Next steps
Section titled “Next steps”- Error codes — the full status, type, and code table with retry guidance
- Provider routing headers — every
X-0G-Provider-*header and its exact value domain - Harden for production — the checks that prevent most of the above