Troubleshooting
On this page
- InvalidIdentityCommitmentRoot when scanning
- Session errors (400, 401, 409, 410)
- 422 when refreshing a proof
- 429 rate limited
- 404 Agent not found
- Agent is registered but not verified
- Signature verification fails (401 from verifier.auth())
- 502 RPC error
- Visa write errors
- Ed25519 agent can’t be deregistered with a scan
- MCP privileged tools are denied
- Foundry build fails against Hub V2
- userDefinedData looks garbled in a custom contract
InvalidIdentityCommitmentRoot when scanning
The document doesn’t match the network. Each network accepts only one kind of document:
- Celo Sepolia (
testnet,11142220): mock passports only. In the Self app, go to Settings → Manage ID documents → Generate Mock Document (guide). - Celo Mainnet (
mainnet,42220): real passports and ID cards only.
A real passport scanned for a testnet session fails with InvalidIdentityCommitmentRoot, and so does a mock passport on mainnet. Start a new session on the network that matches the document: network: "mainnet" for a real passport, or select a mock passport in the Self app for testnet. The CLI defaults to testnet, while the SDK, A2A and MCP default to mainnet.
Session errors (400, 401, 409, 410)
| Response | Cause and fix |
|---|---|
400 Missing session token. Provide Authorization: Bearer <sessionToken>. | The token was not sent as a header. Send Authorization: Bearer <sessionToken>. Tokens in the query string are ignored |
400 on a status route with a valid token | The token belongs to another flow (e.g. a registration token on /deregister/status) |
401 Invalid session token | The token is truncated, altered, or from another deployment. Send the exact sessionToken string from the latest response |
401 on /api/qr/... | A session token was used as a QR token. Use the scanUrl as returned |
409 Cannot export key until registration is complete. Current stage: qr-ready | Export used a token that still says qr-ready: either the human hasn’t finished, or you sent the start token. Poll status until completed, then export with the rotated token from that poll |
409 on /api/agent/register/qr | The session is past qr-ready (already scanned) |
410 Session expired | Sessions last 30 minutes, and the export window is 30 minutes after the poll that saw completed. Start a new session, or keep polling an EVM-mode session to catch a late scan (up to 24 hours) |
Every status poll returns a new session token, so always use the latest one. The SDK (waitForCompletion, exportKey) does this for you. Details: Authentication.
422 when refreshing a proof
Agents whose 32-byte agent ID contains a byte of 0x80 or more can’t be refreshed in place. Deregister and register again, which mints a new agent ID, then update any stored references. A 400 saying the agent has no stored verification config has the same fix. See Proof refresh for which IDs are affected.
429 rate limited
{ "error": "Rate limit exceeded. Try again later.", "retryAfterMs": 42000 }
Limits are per IP. Wait for the Retry-After header (seconds) or retryAfterMs before retrying, and poll every few seconds rather than in a tight loop. A2A reports the same limit as JSON-RPC error -32000. Limits per endpoint: Rate limits.
404 Agent not found
The agent ID was never minted or was burned (deregistered), or you queried the wrong chain. Check the chain ID in the path (42220 for Celo Mainnet, 11142220 for Celo Sepolia).
Agent is registered but not verified
An agent is verified when hasHumanProof && isProofFresh. If hasHumanProof is true but isProofFresh is false, the proof has expired (proofExpiresAt has passed). The NFT stays in place. Renew the proof with requestProofRefresh({ agentId, network }) or POST /api/agent/refresh (Proof refresh). On-chain, isVerifiedAgent() alone does not check expiry, so pair it with isProofFresh().
Also check that the verifier reads the right network. SelfAgentVerifier defaults to mainnet, so an agent registered on testnet needs .network('testnet').
Signature verification fails (401 from verifier.auth())
| Error | Fix |
|---|---|
Timestamp expired or invalid | Timestamps are unix milliseconds and expire after 5 minutes. Sync clocks and sign right before sending |
Invalid signature | The signature covers keccak256(timestamp + METHOD + pathWithQuery + keccak256(body)). Verify the raw body bytes (not re-serialized JSON) and the path plus query, without the host |
Agent not verified on-chain | The key isn’t registered on the verifier’s network, or the agent was deregistered |
Agent's human proof has expired | Refresh the proof |
500 Raw request body unavailable for signature verification | Keep the raw body: express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }) |
503 Agent verification unavailable, try again later | The verifier’s RPC failed. Retry, or set .rpc(url) |
502 RPC error
The API could not read the chain. Retry after a moment. For a visa write, a 502 with txHash means the transaction was broadcast, so check it on-chain before signing a fresh retry.
Visa write errors
401signature errors: sign a freshtimestamp(within 5 minutes, single use) over the exact message in Visa writes.403: the signer is not the NFT owner, the agent’s EVM key or the wallet behind a wallet-based ID.422NOT_ELIGIBLEorREVIEW_REQUIRED: the metrics are below the tier thresholds, or tier 3 needs an approved manual review first.
Ed25519 agent can’t be deregistered with a scan
Ed25519 agents (ed25519, ed25519-linked) get 400 from POST /api/agent/deregister. Instead, the guardian calls guardianRevoke(agentId) or the NFT owner calls selfDeregister(agentId) on the registry.
MCP privileged tools are denied
self_get_identity, self_deregister_agent, self_sign_request and self_authenticated_fetch act as the server’s own agent, so the hosted server at https://agent-api.self.xyz/api/mcp doesn’t offer them to callers. To use them, run your own deployment with SELF_AGENT_PRIVATE_KEY and an MCP key. To sign with your own key instead, use the local server (npx -y @selfxyz/mcp-server) or the SDK.
Foundry build fails against Hub V2
Hub V2 uses the PUSH0 opcode. Build with forge build --evm-version cancun, or set evm_version = "cancun" in foundry.toml.
userDefinedData looks garbled in a custom contract
The Self app passes userDefinedData as a UTF-8 string, so every position is an ASCII character ('0' is 0x30, not 0x00). Parse characters, not raw bytes. See Registration data.
Thanks. What went wrong?
Thanks for your feedback!