SDK integration
On this page
The official SDK is TypeScript: @selfxyz/agent-sdk (0.3.0). It signs requests as an agent, verifies them in a service, drives registration through the REST API and ships the self-agent CLI.
npm install @selfxyz/agent-sdk
The SDK defaults to mainnet (Celo Mainnet, 42220). Pass network: 'testnet' for Celo Sepolia (11142220).
Signing scheme
Every signed request carries these headers:
| Header | Value |
|---|---|
x-self-agent-address | EVM agents: the agent’s address |
x-self-agent-signature | Signature over keccak256(timestamp + METHOD + pathWithQuery + keccak256(body)) (keccak256("") for no body) |
x-self-agent-timestamp | Unix milliseconds. Verifiers reject timestamps older than 5 minutes |
x-self-agent-keytype | Ed25519 agents only: ed25519 |
x-self-agent-key | Ed25519 agents only: the public key (hex) |
EVM agents sign the 32-byte digest with EIP-191 personal_sign; Ed25519 agents sign the same 32 bytes with Ed25519. METHOD is uppercase, and pathWithQuery is the URL path plus query string (no host).
To verify without the SDK, recompute the digest from the raw body, recover the EIP-191 signer from x-self-agent-signature and reject stale timestamps. Then require isVerifiedAgent(zeroPad(signer, 32)) and isProofFresh(getAgentId(...)) on the registry.
Agent side: sign requests
import { SelfAgent } from '@selfxyz/agent-sdk';
const agent = new SelfAgent({ privateKey: process.env.AGENT_PRIVATE_KEY!, network: 'testnet' });
const res = await agent.fetch('https://api.example.com/data', {
method: 'POST',
body: JSON.stringify({ q: 'test' }), // string bodies default to application/json
});
const headers = await agent.signRequest('GET', 'https://api.example.com/x'); // headers only
const info = await agent.getInfo(); // info.isVerified === info.hasHumanProof && info.isProofFresh
SelfAgent takes privateKey (or an ethers signer), network, and optional registryAddress and rpcUrl overrides.
| Member | Returns |
|---|---|
address, agentKey | EVM address; bytes32 registry key (zeroPad(address, 32)) |
fetch(url, init?) / signRequest(method, url, body?) | Signed Response / the x-self-agent-* headers |
isRegistered() | Whether a human proof was recorded (stays true after it expires) |
getInfo() | agentId, isVerified, hasHumanProof, isProofFresh, proofExpiresAt, daysUntilExpiry, isExpiringSoon, nullifier, agentCount, siblingAgentIds |
getCredentials() / getCredentialsFromApi() | ZK-attested credentials, read on-chain / through the API |
getVerificationStrength() | 0 to 100 (100 = passport) |
getAgentCard() / setAgentCard(fields) / toAgentCardDataURI() | Read / write / data URI of the agent card. setAgentCard sends a transaction and works only when the agent key owns its NFT (wallet-free agents) |
requestDeregistration({ disclosures? }) | A deregistration session for this agent |
Ed25519 agents use Ed25519Agent (new Ed25519Agent({ privateKey: '<64 hex>', network: 'testnet' })) with the same fetch, signRequest, isRegistered and getInfo. Its agentKey is the raw public key, and Ed25519Agent.deriveAddress(pubkey) gives the agent’s EVM address.
Service side: verify requests
import express from 'express';
import { SelfAgentVerifier } from '@selfxyz/agent-sdk';
const verifier = SelfAgentVerifier.create()
.network('testnet')
.requireAge(18)
.requireOFAC()
.sybilLimit(3)
.build();
const app = express();
// The signature covers the exact body bytes: keep them as req.rawBody.
app.use(
express.json({
verify: (req, _res, buf) => {
(req as { rawBody?: Buffer }).rawBody = buf;
},
}),
);
app.post('/api/data', verifier.auth(), (req, res) => {
res.json({ agentId: req.agent!.agentId.toString() }); // req.agent: VerifiedAgent
});
Verifier options
| Builder method | Effect | Default |
|---|---|---|
.network(name) / .registry(addr) / .rpc(url) | Chain to read | mainnet |
.requireAge(n) / .requireOFAC() | Minimum disclosed age (18 or 21) / passed OFAC screening. Fetches credentials automatically | Off |
.requireNationality(...codes) | ISO 3166-1 alpha-3 as on the passport, e.g. "USA", "GBR" | Off |
.requireSelfProvider(enabled?) | Proof must come from Self’s provider | On |
.sybilLimit(n) | Max agents per human (0 = unlimited) | 1 |
.rateLimit({ perMinute?, perHour? }) | Per-agent in-memory sliding window | Off |
.replayProtection(enabled?) | Reject reused signatures (10,000-entry cache) | On |
.includeCredentials() | Attach credentials to the result | Off |
.maxAge(ms) / .cacheTtl(ms) | Timestamp window / on-chain cache TTL | 5 min / 1 min |
The same options work as a flat object: SelfAgentVerifier.fromConfig({ network, requireAge, requireOFAC, sybilLimit, ... }).
The verifier checks, in order: timestamp, signature, replay, on-chain isVerifiedAgent and isProofFresh, provider, sybil limit, credentials, rate limit.
Responses from auth()
| Status | When |
|---|---|
401 | Missing headers or a failed check, with { error }, e.g. Invalid signature, Timestamp expired or invalid, Agent not verified on-chain, Agent's human proof has expired, a policy failure |
429 | Per-agent rate limit, with { error, retryAfterMs } |
500 | A request with a body but no raw body |
503 | The chain could not be read |
On success, req.agent holds:
| Field | Type | Description |
|---|---|---|
address | string | Agent’s address (recovered, or derived from the Ed25519 key) |
agentKey | string | bytes32 registry key |
agentId | bigint | On-chain agent ID |
agentCount | bigint | Active agents of the same human |
nullifier | bigint | The human’s nullifier (rate-limit or dedupe per human) |
credentials | object | With includeCredentials(): nationality, olderThan, ofac, … |
Without Express
Call verifier.verify({ signature, timestamp, method, url, body, keytype?, agentKey? }) with the raw header values, url as path plus query and body as the raw string. It returns { valid, agentAddress, agentKey, agentId, agentCount, nullifier, error?, credentials?, proofExpiresAt?, isExpiringSoon? }.
Signature-free check
verifyAgent(agentKey, { network }) returns { verified, agentId, expiresAt } or { verified: false, reason: "NOT_REGISTERED" | "NO_HUMAN_PROOF" | "PROOF_EXPIRED", reauthUrl? }.
Registration, refresh and deregistration
These helpers call the REST API and follow the rotating session token for you. The API base is the apiBase option if set, otherwise the SELF_AGENT_API_BASE environment variable, otherwise https://agent-api.self.xyz.
import { requestRegistration, requestProofRefresh } from '@selfxyz/agent-sdk';
const session = await requestRegistration({
mode: 'wallet-free',
network: 'testnet',
disclosures: { minimumAge: 18, ofac: true },
});
console.log(session.scanUrl, session.humanInstructions); // show to the human
const { agentId, agentAddress } = await session.waitForCompletion(); // { timeoutMs?, pollIntervalMs? }
const privateKey = await session.exportKey(); // EVM modes only, within 30 minutes
const refresh = await requestProofRefresh({ agentId, network: 'testnet' });
const { proofExpiresAt } = await refresh.waitForCompletion();
| Function | Does |
|---|---|
requestRegistration({ mode, network, humanAddress?, ed25519Pubkey?, ed25519Signature?, disclosures? }) | Starts a registration. The session has scanUrl, deepLink, waitForCompletion(), exportKey() |
requestProofRefresh({ agentId, network }) | Renews the proof of an existing agent (same agent ID and key) |
requestDeregistration({ network, agentAddress, disclosures? }) or agent.requestDeregistration() | Starts a deregistration, with waitForCompletion() |
getAgentInfo(agentId, { network }), getAgentsForHuman(address, { network }) | API lookups (also static on SelfAgent) |
Polling defaults to a 30-minute timeout and a 5-second interval. Failed calls throw RegistrationError (with the HTTP status), and an expired session throws ExpiredSessionError. See Registration modes for the modes.
Proof expiry
Proofs expire at min(document expiry, registration + 365 days). After that, isProofFresh is false and verifiers reject the agent. getInfo() exposes proofExpiresAt (unix seconds), daysUntilExpiry and isExpiringSoon (under 30 days; also isProofExpiringSoon()). Renew in place with requestProofRefresh before expiry. Some agent IDs can’t be refreshed: see Proof refresh.
Agent cards and A2A
generateRegistrationJSON(...)builds an ERC-8004 registration document, optionally also a valid A2A Agent Card. See Agent registration JSON.agent.setAgentCard({ name, description?, url?, skills? })stores a card on-chain (NFT owner only), and the API serves it at/api/cards/{chainId}/{agentId}.A2AClientandA2AServerimplement A2A v0.3 JSON-RPC (message/send,tasks/get,tasks/cancel).
More
- CLI environment variables: CLI configuration.
- Full export list: the SDK README.
Thanks. What went wrong?
Thanks for your feedback!