REST API
On this page
- Authentication
- Rate limits
- Quick start
- Registration
- Start a registration
- Poll a registration
- Re-fetch the QR code
- Export the agent key
- Ed25519 challenge
- Check an Ed25519 key
- QR image
- CLI QR handoff
- Deregistration
- Proof refresh
- Identify
- Agent queries
- Agent info
- Verify an agent
- Agent card
- Agents of a wallet
- Agents of a human
- Celo Agent Visa
- Visa reads
- Visa writes
- Discovery
- A2A
- MCP
- Errors
The Self Agent ID API registers, deregisters, refreshes and queries agents in the on-chain registry. It has no API keys: flows that need the human return a session token, and reads are open.
- Base URL:
https://agent-api.self.xyz - OpenAPI 3.1 spec:
openapi.yaml, to import into Postman or generate a client - Machine-readable docs:
GET /llms.txtandGET /api/agent-discovery(see Discovery) - Networks:
mainnetis Celo Mainnet (42220) andtestnetis Celo Sepolia (11142220). Bodies takenetworkand paths takechainId. Any other chain is a400. - Errors: JSON
{ "error": "..." }. Every response carriesAccess-Control-Allow-Origin: *. See Errors.
Authentication
| Kind | Used by | How |
|---|---|---|
| None | Agent queries, visa reads, discovery, A2A, public MCP tools | Nothing to send |
| Session token | Register, deregister, refresh and identify flows | Authorization: Bearer <sessionToken>. Never accepted in the query string. POST /api/agent/register/export also accepts { "token": "..." } in the body |
| QR token | GET /api/qr/{qrToken} (the scanUrl) | Part of the scanUrl path. It renders the QR image and unlocks nothing else |
| Signed request | Visa writes | EIP-191 signature in the body (Visa writes) |
| MCP key | Privileged MCP tools on a deployment you run | Authorization: Bearer <MCP key> (MCP) |
Session tokens are encrypted, expire after 30 minutes and belong to one flow (a registration token sent to /deregister/status is a 400). Every status response returns a rotated sessionToken, so always send the latest one. A token describes the session as it was when that token was issued: the token from the start response still says qr-ready after the human has scanned.
Rate limits
Limits are per client IP. Over a limit you get 429 with a Retry-After header (seconds) and { "error": "...", "retryAfterMs": 12000 }.
| Endpoints | Limit |
|---|---|
Session starts: POST /api/agent/register, /deregister, /refresh, /identify, /register/ed25519-challenge | 10/min each |
Session calls: each GET .../status, GET /api/agent/register/qr, POST /api/agent/register/export, GET /api/agent/register/ed25519-check | 120/min each |
Single-agent reads: info, verify, verify-status, reputation, cards, /.well-known/agent-card.json?agentId=, GET /api/visa/{chainId}/{agentId} | 120/min, shared |
Scans: /api/agent/agents/..., /api/agent/agents-by-nullifier/..., /api/visa/agents | 20/min, shared |
QR images: GET /api/qr/{qrToken}, GET /cli/register | 60/min each |
Refresh link: GET /api/agent/refresh | 20/min |
GET /api/visa/{chainId}/batch | 60/min |
Visa writes: POST /api/visa/claim, /migrate, /request-review | 20 attempts/hour; relayed transactions 5/hour per IP, 2/hour per signer, 50/hour global |
POST /api/a2a (message/send) | 60/min |
POST /api/mcp | 120/min |
Quick start
Register a wallet-free agent on testnet with curl and jq. On testnet the human scans with a mock passport in the Self app.
BASE=https://agent-api.self.xyz
# 1. Start a registration (wallet-free: the server generates the agent key)
RES=$(curl -s -X POST "$BASE/api/agent/register" \
-H "Content-Type: application/json" \
-d '{"mode":"wallet-free","network":"testnet","disclosures":{"minimumAge":18}}')
TOKEN=$(echo "$RES" | jq -r .sessionToken)
# 2. Show the human the QR image (or the deepLink); they scan it with the Self app
echo "$RES" | jq -r .scanUrl
# 3. Poll until the agent is minted. Every response rotates the token: keep the latest.
while true; do
RES=$(curl -s "$BASE/api/agent/register/status" -H "Authorization: Bearer $TOKEN")
TOKEN=$(echo "$RES" | jq -r .sessionToken)
[ "$(echo "$RES" | jq -r .stage)" = qr-ready ] || break
sleep 5
done
echo "$RES" | jq '{stage, agentId, agentAddress, error}'
# 4. Export the generated key with the token from the completing poll (within 30 minutes)
curl -s -X POST "$BASE/api/agent/register/export" -H "Authorization: Bearer $TOKEN"
# → { "privateKey": "0x...", "agentAddress": "0x...", "agentId": 42, "network": "testnet", "mode": "wallet-free" }
Registration
Every flow that needs the human (register, deregister, refresh, identify) works the same way. Each start endpoint requires network in the body, with no default.
POSTthe start endpoint. It returns the session-start fields, includingsessionTokenandscanUrl.- Show the human the QR code: link or embed
scanUrl, renderqrImageBase64, or opendeepLinkon the phone that has the Self app. - Poll the matching
GET .../statuswithAuthorization: Bearer <latest sessionToken>every few seconds untilstageiscompleted. - Registration in an EVM mode: export the generated key.
Start a registration
POST /api/agent/register
| Field | Required | Notes |
|---|---|---|
mode | Yes | linked, wallet-free, ed25519, ed25519-linked, privy, smartwallet (aliases walletfree, smart-wallet). See Registration modes |
network | Yes | mainnet or testnet |
humanAddress | linked, ed25519-linked, privy, smartwallet | Non-zero 0x address; a mixed-case address must have a valid checksum. It is validated in every mode, so omit it rather than sending a placeholder |
ed25519Pubkey | ed25519, ed25519-linked | 64 hex characters, no 0x |
ed25519Signature | ed25519, ed25519-linked | 128 hex characters: the signature over the Ed25519 challenge |
disclosures | No | minimumAge (0, 18, 21) and ofac select the on-chain verification config. nationality, name, date_of_birth, gender and issuing_state ask the Self app to disclose that field. Flags must be JSON booleans |
curl -s -X POST https://agent-api.self.xyz/api/agent/register \
-H "Content-Type: application/json" \
-d '{"mode":"linked","network":"testnet","humanAddress":"0x83fa4380903fecb801F4e123835664973001ff00","disclosures":{"minimumAge":18,"ofac":true}}'
200 returns the session-start fields plus agentAddress, network, mode (canonical name) and configIndex (0 to 5, the verification config selected by minimumAge and ofac).
Errors: 400 invalid input · 413 body over 16 KB · 429 rate limited · 500 Server configuration error or Failed to render QR code · 502 RPC error
Session-start response
Every session-start endpoint (register, deregister, refresh, identify) returns:
| Field | Description |
|---|---|
sessionToken | Encrypted session token. Send it as Authorization: Bearer |
stage | qr-ready |
scanUrl | https://agent-api.self.xyz/api/qr/{qrToken}: a PNG of the QR code, safe to share or embed as <img src> on any origin (QR image) |
deepLink | Self app universal link (https://redirect.self.xyz?selfApp=...) |
qrImageBase64 | The same PNG as bare base64 (no data: prefix). Embed it as data:image/png;base64,<qrImageBase64> |
qrData | The Self app request object, to render the QR yourself with @selfxyz/qrcode |
expiresAt | ISO 8601 session expiry (30 minutes) |
timeRemainingMs | Milliseconds until expiresAt |
humanInstructions | Short steps to show the human |
{
"sessionToken": "SXB5Dx72qaOYHr9CBz-1...",
"stage": "qr-ready",
"scanUrl": "https://agent-api.self.xyz/api/qr/W0yyvGTWGb3uZ1L...",
"deepLink": "https://redirect.self.xyz?selfApp=...",
"qrImageBase64": "iVBORw0KGgoAAAANSUhEUgAA...",
"qrData": {
"version": 2,
"appName": "Self Agent ID",
"scope": "self-agent-id",
"endpoint": "0x043DaCac8b0771DD5b444bCC88f2f8BBDBEdd379",
"endpointType": "staging_celo",
"chainID": 11142220,
"userId": "08af3a49b2f322923de548dc4a50b7a8a20e3df5",
"userIdType": "hex",
"userDefinedData": "W108af...",
"disclosures": { "minimumAge": 18 }
},
"agentAddress": "0x08Af3A49b2f322923DE548dC4A50B7A8A20E3DF5",
"network": "testnet",
"mode": "wallet-free",
"configIndex": 1,
"expiresAt": "2026-10-27T07:06:08.418Z",
"timeRemainingMs": 1799999,
"humanInstructions": [
"Open the Self app on your phone.",
"Scan the QR code displayed below or open the deep link.",
"Follow the prompts to scan your passport.",
"Wait for the proof to be submitted on-chain."
]
}
Poll a registration
GET /api/agent/register/status
Authorization: Bearer <sessionToken>
The API checks the registry on-chain. stage is qr-ready until the agent is minted, then completed. Each response carries sessionToken (rotated), stage, expiresAt, timeRemainingMs, humanInstructions and agentAddress; a completed response adds agentId.
{
"sessionToken": "Xk2p...",
"stage": "completed",
"expiresAt": "2026-10-27T07:20:02.114Z",
"timeRemainingMs": 1800000,
"agentAddress": "0x08Af3A49b2f322923DE548dC4A50B7A8A20E3DF5",
"agentId": 42,
"credentials": {
"nationality": null,
"issuingState": null,
"olderThan": 18,
"ofac": [false, false, false],
"dateOfBirth": null,
"gender": null,
"expiryDate": null
},
"humanInstructions": ["Registration complete. Your agent is now verified on-chain."]
}
- Export window. The poll that first sees
completedresetsexpiresAtto 30 minutes from then, so the key can still be exported. Only that poll includescredentials: undisclosed fields arenull,olderThanis0when no age was proven, and the object is omitted if the credentials could not be read. - Late scans. The Self deep link never expires. A session holding a server-generated key can be polled for up to 24 hours after
expiresAt. If the agent was minted in the meantime, the poll answerscompletedwith a fresh 30-minute export window; otherwise it answers410.
Errors: 400 missing Authorization header or not a registration token · 401 invalid token · 410 session expired · 429 · 500 · 502
Re-fetch the QR code
GET /api/agent/register/qr
Authorization: Bearer <sessionToken>
Returns { deepLink, qrData, scanUrl } (with a fresh scanUrl) for a session that is still qr-ready.
Errors: 400 · 401 · 409 session past qr-ready · 410 · 429 · 500
Export the agent key
POST /api/agent/register/export
Authorization: Bearer <sessionToken>
Returns the server-generated private key of a linked, wallet-free, privy or smartwallet agent once the session is completed. Use the rotated token from the poll that saw completed, because the start token still says qr-ready and gets 409. The token can also be sent in the body as { "token": "..." }. Every response, errors included, has Cache-Control: no-store.
{
"privateKey": "0x4c0883a69102937d6231471b5dbb6204fe512961708279f2e3e8a5d4b8e3e0a1",
"agentAddress": "0x08Af3A49b2f322923DE548dC4A50B7A8A20E3DF5",
"agentId": 42,
"network": "testnet",
"mode": "wallet-free"
}
Errors: 400 no token, not a registration token, or an ed25519 / ed25519-linked session (the agent already holds its key) · 401 · 409 Cannot export key until registration is complete. Current stage: qr-ready · 410 export window over · 413 · 429 · 500
Ed25519 challenge
POST /api/agent/register/ed25519-challenge
Step 1 of an ed25519 or ed25519-linked registration. Body: { "pubkey": "<64 hex>", "network": "testnet" }, plus humanAddress (the guardian wallet) for ed25519-linked. Send the same humanAddress to /register. Returns { "challengeHash": "0x...", "nonce": "0" }.
Sign the 32 raw bytes of challengeHash (hex-decode it, don’t sign the hex string) with the agent’s Ed25519 key, then call POST /api/agent/register with ed25519Pubkey and the 128-hex ed25519Signature:
import * as ed from '@noble/ed25519'; // v3
const BASE = 'https://agent-api.self.xyz';
const post = (path, body) =>
fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then((r) => r.json());
const secretKey = ed.utils.randomSecretKey(); // the agent's key: keep it
const pubkey = Buffer.from(await ed.getPublicKeyAsync(secretKey)).toString('hex');
const { challengeHash } = await post('/api/agent/register/ed25519-challenge', {
pubkey,
network: 'testnet',
});
const signature = await ed.signAsync(Buffer.from(challengeHash.slice(2), 'hex'), secretKey);
const session = await post('/api/agent/register', {
mode: 'ed25519',
network: 'testnet',
ed25519Pubkey: pubkey,
ed25519Signature: Buffer.from(signature).toString('hex'),
});
console.log(session.scanUrl); // the human scans this; then poll /api/agent/register/status
Ed25519 sessions have nothing to export.
Errors: 400 · 413 · 429 · 502
Check an Ed25519 key
GET /api/agent/register/ed25519-check?pubkey=<64 hex>&network=testnet
Returns { "registered": false } or { "registered": true, "agentId": 42, "agentAddress": "0x..." }. Both parameters are required. Errors: 400 · 429 · 502
QR image
GET /api/qr/{qrToken}
The image behind every scanUrl: a PNG of the session’s deep link, roughly 940 to 970 px square (scale it down with CSS). It accepts only QR tokens (a session token gets 401). It is served with Access-Control-Allow-Origin: *, Cross-Origin-Resource-Policy: cross-origin, Content-Disposition: inline, X-Content-Type-Options: nosniff and Cache-Control: no-store, so it embeds on any page:
<img
src="https://agent-api.self.xyz/api/qr/W0yyvGTWGb3uZ1L..."
alt="Scan with the Self app"
width="240"
/>
Errors: 401 not a QR token · 410 session expired · 429 · 500 Failed to render QR code
CLI QR handoff
GET /cli/register?payload=<base64url JSON>
The URL printed by self-agent register open. The CLI builds payload (at most 8192 characters), and the route returns the Self app QR code as a PNG with the same headers as /api/qr. It renders only linked and wallet-free requests for this service’s registry, app name (Self Agent ID) and scope (self-agent-id).
Errors: 400 invalid payload · 410 payload expiresAt passed · 429 · 500
Deregistration
POST /api/agent/deregister
GET /api/agent/deregister/status (Authorization: Bearer)
The human proves again with the Self app, and the registry burns the agent NFT. Body: network, plus agentId (number or decimal string) and/or agentAddress, and optional disclosures (use the ones from registration).
curl -s -X POST https://agent-api.self.xyz/api/agent/deregister \
-H "Content-Type: application/json" \
-d '{"network":"testnet","agentId":42}'
200 returns the session-start fields plus agentAddress, agentId, network and configIndex. The status poll answers completed, with agentAddress and agentId, once the agent has no human proof on-chain.
Ed25519 agents can’t be deregistered with a scan (400). Instead, the guardian calls guardianRevoke(agentId) or the NFT owner calls selfDeregister(agentId) on the registry.
Errors (start): 400 · 404 agent not found · 413 · 429 · 500 · 502. Errors (status): 400 · 401 · 410 · 429 · 500 · 502
Proof refresh
Human proofs expire at proofExpiresAt. A refresh renews the proof of an existing agent and keeps its agent ID and key.
POST /api/agent/refresh
GET /api/agent/refresh/status (Authorization: Bearer)
GET /api/agent/refresh?agentId=42&network=testnet
POSTbody:agentId(number or decimal string),networkand optionaldisclosures. The disclosures default to the agent’s on-chain config, and if sent they must select the same config.200returns the session-start fields plusagentId,networkandconfigIndex.- Status answers
qr-ready, thencompletedwith the newproofExpiresAt, orfailedwitherrorif the agent was deregistered first. GETwith a query is a shareable refresh link: a302straight to the Self app request (the SDK uses it as its re-auth URL). It acceptschainId=42220|11142220instead ofnetwork, and an optionalregistrythat must match this service’s registry.
Errors: 400 invalid input, a disclosure mismatch, or an agent with no stored verification config (for example one verified through an external proof provider; deregister and register again) · 404 · 413 · 422 (below) · 429 · 500 · 502
Identify
Find all agents of a human without knowing an agent ID. Nothing changes on-chain.
POST /api/agent/identify {"network":"testnet"}
GET /api/agent/identify/status (Authorization: Bearer)
The start returns the session-start fields plus network. Once the human has scanned, the status is completed with nullifier (decimal string) and agentCount. Always send the newest token: it records how far the chain has been searched, so each poll only checks new blocks. Then list the agents with agents-by-nullifier.
Agent queries
Read-only GET endpoints with no auth. Every per-agent route returns 404 { "error": "Agent not found" } for an agent that was never minted or was burned, and 502 { "error": "RPC error" } when the chain can’t be read. isVerified (or verified) always means hasHumanProof && isProofFresh.
| Endpoint | Returns |
|---|---|
/api/agent/info/{chainId}/{agentId} | Key, address, proof state, provider, registeredAtBlock, strength, credentials |
/api/agent/verify/{chainId}/{agentId} | Proof state, provider, isSelfProvider, strength, humanNullifier, agentCountForHuman |
/api/verify-status/{chainId}/{agentId} | verified, hasHumanProof, isProofFresh, proofExpiresAt, proofType, registeredAtBlock, providerAddress |
/api/reputation/{chainId}/{agentId} | score (provider strength, 0 to 100), hasProof, isVerified, isProofFresh, proofExpiresAt, providerName, proofType |
/api/cards/{chainId}/{agentId} | ERC-8004 agent card (below). Cacheable for 60 s |
/api/agent/agents/{chainId}/{address} | Agents a wallet owns, plus the agent whose key is that address |
/api/agent/agents-by-nullifier/{chainId}/{nullifier} | Active agents of one human |
Agent info
curl -s https://agent-api.self.xyz/api/agent/info/11142220/5
{
"agentId": 5,
"chainId": 11142220,
"agentKey": "0x000000000000000000000000a7f2b8a0ba50b099ee2ad16a91176bed96e103f7",
"agentAddress": "0xA7F2B8A0Ba50b099Ee2ad16A91176bED96E103F7",
"isVerified": true,
"hasHumanProof": true,
"isProofFresh": true,
"proofExpiresAt": "2027-02-27T05:50:54.000Z",
"proofProvider": "0x5E61c3051Bf4115F90AacEAE6212bc419f8aBB6c",
"registeredAtBlock": 18887466,
"verificationStrength": 100,
"strengthLabel": "passport",
"credentials": { "nationality": null, "olderThan": 0, "ofac": [false, false, false] },
"network": "testnet"
}
registeredAtBlock is a block number, not a timestamp (Celo blocks are about 1 second apart). agentKey is the zero-padded address for EVM agents and the raw public key for Ed25519 agents. Undisclosed credentials are null.
Verify an agent
curl -s https://agent-api.self.xyz/api/agent/verify/11142220/5
{
"agentId": 5,
"chainId": 11142220,
"isVerified": true,
"hasHumanProof": true,
"isProofFresh": true,
"proofExpiresAt": "2027-02-27T05:50:54.000Z",
"proofProvider": "0x5E61c3051Bf4115F90AacEAE6212bc419f8aBB6c",
"isSelfProvider": true,
"verificationStrength": 100,
"strengthLabel": "passport",
"humanNullifier": "10532525450741824009985382101719946210697770609024411207137570480189113860982",
"agentCountForHuman": 13
}
Agent card
GET /api/cards/{chainId}/{agentId} returns the agent’s ERC-8004 registration document: fields the agent declared in its on-chain metadata, a services list that ends with the registry’s own A2A endpoint (registry-a2a), and a selfProtocol block rebuilt from live contract state (agentId, registry, chainId, agentKey, agentAddress, isVerified, proofExpiresAt, registeredAtBlock, proofProvider, providerName, verificationStrength, trustModel, credentials). The same card is served at /.well-known/agent-card.json?agentId=5&chainId=11142220, and /.well-known/a2a/{agentId}?chain=11142220 redirects (307) to it.
Agents of a wallet
curl -s https://agent-api.self.xyz/api/agent/agents/11142220/0xA7F2B8A0Ba50b099Ee2ad16A91176bED96E103F7
{
"humanAddress": "0xA7F2B8A0Ba50b099Ee2ad16A91176bED96E103F7",
"chainId": 11142220,
"agents": [
{
"agentId": 5,
"agentKey": "0x000000000000000000000000a7f2b8a0ba50b099ee2ad16a91176bed96e103f7",
"agentAddress": "0xA7F2B8A0Ba50b099Ee2ad16A91176bED96E103F7",
"isOwner": true,
"isAgentKey": true,
"isVerified": true,
"hasHumanProof": true,
"isProofFresh": true,
"proofExpiresAt": "2027-02-27T05:50:54.000Z"
}
],
"totalCount": 1,
"ownedCount": 1,
"complete": true
}
The registry is not enumerable, so the API finds owned IDs with a bounded scan. complete: false means it found fewer than ownedCount (the on-chain balance). A zero, ICAP or unprefixed address is a 400 Invalid address.
Agents of a human
GET /api/agent/agents-by-nullifier/{chainId}/{nullifier} returns { nullifier, chainId, agents, totalCount, activeAgentCount, complete }. Each agent has agentId, agentKey, agentAddress, isVerified, hasHumanProof, isProofFresh and proofExpiresAt; activeAgentCount is the registry’s own counter for this human. Get a nullifier from Identify or /api/agent/verify.
Celo Agent Visa
Endpoints for the Celo Agent Visa. Visa agent IDs are decimal strings: a registry agent ID, or a wallet-based ID (uint160(wallet), too large for a JSON number).
Visa reads
| Endpoint | Returns |
|---|---|
GET /api/visa/{chainId}/{agentId} | tier, tierName, metrics (transactionCount, volumeUsd, lastUpdated), eligibility and thresholds per tier, reviewRequestedTier, manualReviewApproved. 404 when the ID has neither a visa nor a registry agent |
GET /api/visa/{chainId}/batch?agents=1,5,66 | { "agents": [{ "agentId": "1", "tier": 0, "tierName": "None" }, ...] }. At most 100 IDs (counted before duplicates are removed); duplicates are answered once, in first-seen order; any invalid ID is a 400 |
GET /api/visa/agents?wallet=0x...&chainId=11142220 | { "agents": [{ "agentId": "5", "chainId": 11142220, "isWalletBased": false }], "complete": true }: registry agents the wallet owns or is the key of, plus its wallet-based ID if that holds a visa. complete: false means a partial scan |
Visa writes
POST /api/visa/claim, /api/visa/migrate and /api/visa/request-review are relayed, so the server pays the gas. Each request is authorized by an EIP-191 personal_sign signature over this exact message. Join the lines with \n and no trailing newline, write IDs and the chain ID in canonical decimal, and add a newAgentId line only for migrate:
Self Agent ID visa request
action: claim
chainId: 11142220
agentId: 66
targetTier: 1
timestamp: 1793000000000
The body carries the signed fields, signature (0x + 130 hex) and the same timestamp (unix ms). The timestamp must be within 5 minutes of server time, and each signed message is accepted once. The signer must be the agent’s NFT owner, the agent’s own EVM key, or, for a wallet-based ID, that wallet.
| Endpoint | Other body fields | Success response |
|---|---|---|
/api/visa/claim | chainId, agentId, targetTier (1 to 3), agentWallet? (must be the signer) | { success, agentId, previousTier, newTier, wallet, txHash, blockNumber } |
/api/visa/migrate | chainId, oldAgentId (the signer’s wallet-based ID), newAgentId, connectedWallet (the signer), targetTier? (default 1) | { success, oldAgentId, newAgentId, migratedWallet, migratedTxCount, newTier, txHash } |
/api/visa/request-review | chainId, agentId, targetTier (2 or 3) | { success, agentId, targetTier, txHash } |
For migrate, the signed agentId line is the old wallet-based ID and newAgentId is the registry agent that receives the visa.
import { Wallet } from 'ethers';
const wallet = new Wallet(process.env.OWNER_PRIVATE_KEY); // owns agent 66
const req = { chainId: '11142220', agentId: '66', targetTier: 1, timestamp: Date.now() };
const message = [
'Self Agent ID visa request',
'action: claim',
`chainId: ${req.chainId}`,
`agentId: ${req.agentId}`,
`targetTier: ${req.targetTier}`,
`timestamp: ${req.timestamp}`,
].join('\n');
const res = await fetch('https://agent-api.self.xyz/api/visa/claim', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...req, signature: await wallet.signMessage(message) }),
});
console.log(res.status, await res.json());
Errors: 400 · 401 missing, malformed, expired or reused signature · 403 signer not authorized · 404 · 409 already at or above the tier, new agent already has a visa, or review already requested · 413 · 422 business rule: { "code": "NOT_ELIGIBLE", "metrics": {...}, "required": {...} }, { "code": "REVIEW_REQUIRED", "reviewRequested": false }, or no fresh human proof for tiers 2 and 3 · 429 · 502 RPC or transaction failure, with txHash when a transaction was already broadcast · 503 relayer not configured
Discovery
| Method | Path | Returns |
|---|---|---|
| GET | / | Accept: application/json → discovery document; Accept: text/plain → llms.txt; otherwise 308 to these docs |
| GET | /api/agent-discovery | Discovery document: modes, aliases, decision tree, networks, endpoints, SDK package. Cacheable for 1 h |
| GET | /.well-known/self-agent-id.json | Same discovery document |
| GET | /llms.txt | Plain-text usage guide for LLMs |
| GET | /agents.json | Index of the machine-readable entry points (not a list of agents) |
| GET | /api/agent/bootstrap | Minimal OpenAPI 3.1 document of the registration endpoints, for agents that load an API as tools |
| GET | /.well-known/agent-registration | 307 → /api/agent/bootstrap |
| GET | /.well-known/agent-card.json | The registry’s own A2A card; with ?agentId=&chainId= (default chain 42220), that agent’s card |
| GET | /.well-known/a2a/{agentId}?chain= | 307 → /api/cards/{chainId}/{agentId} (default chain 42220) |
| GET | /api/health | { "status": "ok", "service": "self-agent-id", "timestamp": "..." } |
Unknown /api/* paths return a JSON 404 with links to these entry points.
A2A
POST /api/a2a
A2A v0.3.0 over JSON-RPC 2.0. Methods: message/send, tasks/get, tasks/cancel (no streaming or push notifications). Parts use kind. The intent comes from a data part, a text part holding JSON, or plain English (“look up agent 5 on testnet”).
curl -s -X POST https://agent-api.self.xyz/api/a2a \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"data","data":{"intent":"verify","agentId":5,"chainId":11142220}}]}}}'
The result is an A2A task (kind: "task") whose status.message.parts hold a text summary and a data part (agentId, isVerified, hasHumanProof, isProofFresh, proofExpiresAt, daysUntilExpiry, …).
| Intent | Data fields |
|---|---|
register | mode, network, humanAddress?, ed25519Pubkey?, ed25519Signature?, disclosures? (default none) |
status | sessionToken from the previous reply’s data part |
lookup, verify, freshness, refresh-proof, deregister | agentId, plus chainId or network |
help | None |
- Session intents (
register,refresh-proof,deregister) return the session token in the task’sdatapart with apollHint. Poll with{ "intent": "status", "sessionToken": "..." }, each time with the newest token. - Intents without a network or chain use
mainnet. - An optional
X-Agent-Idheader makes the call fail with403unless that agent is verified on Celo Mainnet. message/sendis limited to 60/min per IP.lookup,verify,freshness,deregisterand theX-Agent-Idcheck also spend the shared 120/min read budget, and session intents also count against the limit of the route they call. A rate limit answers HTTP429withRetry-Afterand JSON-RPC error-32000(data.retryAfterMs).- Other errors are JSON-RPC errors with HTTP
400(message/streamisUNSUPPORTED_OPERATION,-32004),403,404(unknown method or task),409(task already finished),413(body over 64 KB),415(notapplication/json) or500.
MCP
POST /api/mcp
A stateless Model Context Protocol server over Streamable HTTP. Point an MCP client at https://agent-api.self.xyz/api/mcp. Raw requests send Accept: application/json, text/event-stream. GET and DELETE answer 405, and OPTIONS answers the CORS preflight with 204.
- Public tools:
self_lookup_agent,self_list_agents_for_human,self_register_agent,self_check_registration,self_verify_agent,self_verify_request. - Privileged tools (
self_get_identity,self_deregister_agent,self_sign_request,self_authenticated_fetch) act as the server’s own agent. They work only on a deployment you run withSELF_AGENT_PRIVATE_KEYset, called withAuthorization: Bearer <MCP key>. - Rate limit: 120/min per IP, and each tool call also counts against the limit of the REST endpoint it uses. Body limit: 1 MB.
Tool arguments and client setup: Using the MCP server.
Errors
| Code | Meaning |
|---|---|
400 | Invalid input, missing Authorization header, a token for another flow, or an unsupported chain |
401 | Invalid session token, QR token or visa signature |
403 | Visa signer not authorized, or A2A X-Agent-Id agent not verified |
404 | Agent not found (never minted or burned) |
409 | Wrong state: export before completed, QR after the scan, visa tier already reached |
410 | Session expired (30 minutes; for export, 30 minutes after completion) |
413 | Body too large: 16 KB (64 KB for A2A, 1 MB for MCP) |
422 | Business rule not met: refresh ID limitation, visa eligibility |
429 | Rate limited. Wait Retry-After seconds (or retryAfterMs) |
500 | Server error with a generic message, e.g. Failed to render QR code |
502 | RPC error: the chain could not be read. Retry |
503 | Visa relayer not configured on this deployment |
See Troubleshooting for fixes.
Thanks. What went wrong?
Thanks for your feedback!