Skip to content

Start typing to search the documentation.

Troubleshooting

On this page

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)

ResponseCause 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 tokenThe token belongs to another flow (e.g. a registration token on /deregister/status)
401 Invalid session tokenThe 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-readyExport 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/qrThe session is past qr-ready (already scanned)
410 Session expiredSessions 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())

ErrorFix
Timestamp expired or invalidTimestamps are unix milliseconds and expire after 5 minutes. Sync clocks and sign right before sending
Invalid signatureThe 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-chainThe key isn’t registered on the verifier’s network, or the agent was deregistered
Agent's human proof has expiredRefresh the proof
500 Raw request body unavailable for signature verificationKeep the raw body: express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })
503 Agent verification unavailable, try again laterThe 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

  • 401 signature errors: sign a fresh timestamp (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.
  • 422 NOT_ELIGIBLE or REVIEW_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.

Was this page helpful?