Skip to content

Start typing to search the documentation.

REST API

On this page

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.txt and GET /api/agent-discovery (see Discovery)
  • Networks: mainnet is Celo Mainnet (42220) and testnet is Celo Sepolia (11142220). Bodies take network and paths take chainId. Any other chain is a 400.
  • Errors: JSON { "error": "..." }. Every response carries Access-Control-Allow-Origin: *. See Errors.

Authentication

KindUsed byHow
NoneAgent queries, visa reads, discovery, A2A, public MCP toolsNothing to send
Session tokenRegister, deregister, refresh and identify flowsAuthorization: Bearer <sessionToken>. Never accepted in the query string. POST /api/agent/register/export also accepts { "token": "..." } in the body
QR tokenGET /api/qr/{qrToken} (the scanUrl)Part of the scanUrl path. It renders the QR image and unlocks nothing else
Signed requestVisa writesEIP-191 signature in the body (Visa writes)
MCP keyPrivileged MCP tools on a deployment you runAuthorization: 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 }.

EndpointsLimit
Session starts: POST /api/agent/register, /deregister, /refresh, /identify, /register/ed25519-challenge10/min each
Session calls: each GET .../status, GET /api/agent/register/qr, POST /api/agent/register/export, GET /api/agent/register/ed25519-check120/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/agents20/min, shared
QR images: GET /api/qr/{qrToken}, GET /cli/register60/min each
Refresh link: GET /api/agent/refresh20/min
GET /api/visa/{chainId}/batch60/min
Visa writes: POST /api/visa/claim, /migrate, /request-review20 attempts/hour; relayed transactions 5/hour per IP, 2/hour per signer, 50/hour global
POST /api/a2a (message/send)60/min
POST /api/mcp120/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.

  1. POST the start endpoint. It returns the session-start fields, including sessionToken and scanUrl.
  2. Show the human the QR code: link or embed scanUrl, render qrImageBase64, or open deepLink on the phone that has the Self app.
  3. Poll the matching GET .../status with Authorization: Bearer <latest sessionToken> every few seconds until stage is completed.
  4. Registration in an EVM mode: export the generated key.

Start a registration

POST /api/agent/register
FieldRequiredNotes
modeYeslinked, wallet-free, ed25519, ed25519-linked, privy, smartwallet (aliases walletfree, smart-wallet). See Registration modes
networkYesmainnet or testnet
humanAddresslinked, ed25519-linked, privy, smartwalletNon-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
ed25519Pubkeyed25519, ed25519-linked64 hex characters, no 0x
ed25519Signatureed25519, ed25519-linked128 hex characters: the signature over the Ed25519 challenge
disclosuresNominimumAge (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:

FieldDescription
sessionTokenEncrypted session token. Send it as Authorization: Bearer
stageqr-ready
scanUrlhttps://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)
deepLinkSelf app universal link (https://redirect.self.xyz?selfApp=...)
qrImageBase64The same PNG as bare base64 (no data: prefix). Embed it as data:image/png;base64,<qrImageBase64>
qrDataThe Self app request object, to render the QR yourself with @selfxyz/qrcode
expiresAtISO 8601 session expiry (30 minutes)
timeRemainingMsMilliseconds until expiresAt
humanInstructionsShort 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 completed resets expiresAt to 30 minutes from then, so the key can still be exported. Only that poll includes credentials: undisclosed fields are null, olderThan is 0 when 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 answers completed with a fresh 30-minute export window; otherwise it answers 410.

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
  • POST body: agentId (number or decimal string), network and optional disclosures. The disclosures default to the agent’s on-chain config, and if sent they must select the same config. 200 returns the session-start fields plus agentId, network and configIndex.
  • Status answers qr-ready, then completed with the new proofExpiresAt, or failed with error if the agent was deregistered first.
  • GET with a query is a shareable refresh link: a 302 straight to the Self app request (the SDK uses it as its re-auth URL). It accepts chainId=42220|11142220 instead of network, and an optional registry that 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.

EndpointReturns
/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

EndpointReturns
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.

EndpointOther body fieldsSuccess response
/api/visa/claimchainId, agentId, targetTier (1 to 3), agentWallet? (must be the signer){ success, agentId, previousTier, newTier, wallet, txHash, blockNumber }
/api/visa/migratechainId, oldAgentId (the signer’s wallet-based ID), newAgentId, connectedWallet (the signer), targetTier? (default 1){ success, oldAgentId, newAgentId, migratedWallet, migratedTxCount, newTier, txHash }
/api/visa/request-reviewchainId, 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

MethodPathReturns
GET/Accept: application/json → discovery document; Accept: text/plain → llms.txt; otherwise 308 to these docs
GET/api/agent-discoveryDiscovery document: modes, aliases, decision tree, networks, endpoints, SDK package. Cacheable for 1 h
GET/.well-known/self-agent-id.jsonSame discovery document
GET/llms.txtPlain-text usage guide for LLMs
GET/agents.jsonIndex of the machine-readable entry points (not a list of agents)
GET/api/agent/bootstrapMinimal OpenAPI 3.1 document of the registration endpoints, for agents that load an API as tools
GET/.well-known/agent-registration307 → /api/agent/bootstrap
GET/.well-known/agent-card.jsonThe 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, …).

IntentData fields
registermode, network, humanAddress?, ed25519Pubkey?, ed25519Signature?, disclosures? (default none)
statussessionToken from the previous reply’s data part
lookup, verify, freshness, refresh-proof, deregisteragentId, plus chainId or network
helpNone
  • Session intents (register, refresh-proof, deregister) return the session token in the task’s data part with a pollHint. Poll with { "intent": "status", "sessionToken": "..." }, each time with the newest token.
  • Intents without a network or chain use mainnet.
  • An optional X-Agent-Id header makes the call fail with 403 unless that agent is verified on Celo Mainnet.
  • message/send is limited to 60/min per IP. lookup, verify, freshness, deregister and the X-Agent-Id check 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 HTTP 429 with Retry-After and JSON-RPC error -32000 (data.retryAfterMs).
  • Other errors are JSON-RPC errors with HTTP 400 (message/stream is UNSUPPORTED_OPERATION, -32004), 403, 404 (unknown method or task), 409 (task already finished), 413 (body over 64 KB), 415 (not application/json) or 500.

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 with SELF_AGENT_PRIVATE_KEY set, called with Authorization: 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

CodeMeaning
400Invalid input, missing Authorization header, a token for another flow, or an unsupported chain
401Invalid session token, QR token or visa signature
403Visa signer not authorized, or A2A X-Agent-Id agent not verified
404Agent not found (never minted or burned)
409Wrong state: export before completed, QR after the scan, visa tier already reached
410Session expired (30 minutes; for export, 30 minutes after completion)
413Body too large: 16 KB (64 KB for A2A, 1 MB for MCP)
422Business rule not met: refresh ID limitation, visa eligibility
429Rate limited. Wait Retry-After seconds (or retryAfterMs)
500Server error with a generic message, e.g. Failed to render QR code
502RPC error: the chain could not be read. Retry
503Visa relayer not configured on this deployment

See Troubleshooting for fixes.

Was this page helpful?