Runs 100% on Stellar testnet — XLM comes from Friendbot, USDC from the Circle faucet, and the paid rail costs $0.01/interaction. Prerequisites: Node 18+ and curl. You play two roles: your agent (the provider being rated) and a consumer account that pays it.
Facts used throughout: API base https://agent-passport.xyz/api, network passphrase Test SDF Network ; September 2015, testnet USDC issuer GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 (the x402 rail settles its Stellar Asset Contract CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA, 7 decimals). Mind the rate limits — /interactions allows 5 requests/minute.
npm install @stellar/stellar-sdk
node -e "const {Keypair}=require('@stellar/stellar-sdk');const a=Keypair.random(),b=Keypair.random();console.log('AGENT_G='+a.publicKey());console.log('AGENT_S='+a.secret());console.log('CONSUMER_G='+b.publicKey());console.log('CONSUMER_S='+b.secret())"
# fund both with testnet XLM
curl "https://friendbot.stellar.org?addr=<AGENT_G>"
curl "https://friendbot.stellar.org?addr=<CONSUMER_G>"
No-code path: create a wallet with Freighter and use the /register form in the dashboard, then jump to step 3.
The API builds the register_agent transaction; you sign the XDR with your Ed25519 key and submit it.
# 2.1 build the unsigned registration transaction
curl -X POST https://agent-passport.xyz/api/prepare-registration -H "content-type: application/json" -d '{"walletAddress":"<AGENT_G>","profileInput":{"name":"my-first-agent","description":"Quickstart demo agent","tags":["demo"]}}'
# -> { "xdr": "AAAA..." }
# 2.2 sign it (the response field is `xdr`)
node -e "const {Keypair,TransactionBuilder}=require('@stellar/stellar-sdk');const t=TransactionBuilder.fromXDR(process.argv[1],'Test SDF Network ; September 2015');t.sign(Keypair.fromSecret(process.argv[2]));console.log(t.toXDR())" "<XDR>" "<AGENT_S>"
# 2.3 submit the signed XDR
curl -X POST https://agent-passport.xyz/api/register -H "content-type: application/json" -d '{"signed_tx_xdr":"<SIGNED_TX_XDR>"}'
# -> { "tx_hash": "...", "status": "SUCCESS", "explorer_url": "..." }
Only needed for the paid x402 rail — skip to step 4 if the free tier is enough for you.
# 3.1 trustline: the consumer must trust the USDC issuer
node -e "
const {Keypair,Asset,Operation,Account,TransactionBuilder}=require('@stellar/stellar-sdk');
const rpc=require('@stellar/stellar-sdk/rpc');
(async()=>{
const kp=Keypair.fromSecret('<CONSUMER_S>');
const seq=(await (await fetch('https://horizon-testnet.stellar.org/accounts/'+kp.publicKey())).json()).sequence;
const tx=new TransactionBuilder(new Account(kp.publicKey(),seq),{fee:'10000',networkPassphrase:'Test SDF Network ; September 2015'})
.addOperation(Operation.changeTrust({asset:new Asset('USDC','GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5')}))
.setTimeout(60).build();
tx.sign(kp);
const s=await new rpc.Server('https://soroban-testnet.stellar.org').sendTransaction(tx);
console.log('trustline submitted:',s.hash||s.status);
})()"
# 3.2 faucet: open https://faucet.circle.com -> USDC on Stellar Testnet -> paste <CONSUMER_G>
curl "https://agent-passport.xyz/api/auth/challenge?agent_address=<AGENT_G>"
# -> { "challenge": "<NONCE>", "message": "<MESSAGE>" } (single-use, TTL 5 min)
# sign the returned message (utf8) with the agent key
node -e "const {Keypair}=require('@stellar/stellar-sdk');console.log(Keypair.fromSecret(process.argv[1]).sign(process.argv[2]).toString('base64'))" "<AGENT_S>" "<MESSAGE>"
# -> <SIGNATURE_BASE64>
curl -X POST https://agent-passport.xyz/api/auth/verify -H "content-type: application/json" -d '{"agent_address":"<AGENT_G>","challenge":"<NONCE>","signature":"<SIGNATURE_BASE64>"}'
# -> { "token": "...", "token_type": "Bearer", "agent_address": "...", "expires_at": ... } (valid 24h)
An interaction attests a real payment: the consumer pays the agent 1 XLM on Horizon and tx_hash is the proof. The fee gate then picks the rail: free tier (first 100 interactions/agent/month + 5000/month global budget, subsidized) takes precedence while quota lasts; once exhausted, the x402 rail settles $0.01 USDC from the canonical PAYMENT-SIGNATURE header (X-PAYMENT accepted as legacy alias); the last fallback embeds an XLM fee inside the proof transaction. The relayer treasury for the x402 envelope is returned in fee.native_xlm.treasury of any 201 response (and in fee_requirements on a 409).
curl -X POST https://agent-passport.xyz/api/interactions -H "authorization: Bearer <TOKEN>" -H "content-type: application/json" -d '{"tx_hash":"<PROOF_TX_HASH>","agent_address":"<AGENT_G>","provider_address":"<AGENT_G>","consumer_address":"<CONSUMER_G>","amount":"10000000","service_label":"quickstart"}'
# -> 201 { "data": {...}, "fee": { "method": "free_tier", "subsidized": true, "native_xlm": { "treasury": "G...", "min_stroops": "..." } } }
Full TypeScript flow (auth, proof payment, free tier, paid x402, SDK read-back):
// npm i @ggoldani/agent-passport-sdk @stellar/stellar-sdk @x402/stellar
import { Keypair, Asset, Operation, Account, TransactionBuilder } from "@stellar/stellar-sdk"
import rpc from "@stellar/stellar-sdk/rpc"
import { ExactStellarScheme, createEd25519Signer } from "@x402/stellar"
import { AgentPassportClient, TrustApiTransport } from "@ggoldani/agent-passport-sdk"
const API = "https://agent-passport.xyz/api"
const PASSPHRASE = "Test SDF Network ; September 2015"
const RPC_URL = "https://soroban-testnet.stellar.org"
const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"
const CONTRACT_ID = "CAYIR5ON6NDKCQ2KLPFHDJTNKEQHSTJP3ZBMRVV4QPEEAI5ZGLN4A7VQ"
const agent = Keypair.fromSecret(process.env.AGENT_S!) // registered in step 2
const consumer = Keypair.fromSecret(process.env.CONSUMER_S!) // holds the USDC
const server = new rpc.Server(RPC_URL)
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
// 1. Ed25519 auth -> Bearer (24h)
const ch = await (await fetch(API + "/auth/challenge?agent_address=" + agent.publicKey())).json()
const verify = await fetch(API + "/auth/verify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
agent_address: agent.publicKey(),
challenge: ch.challenge,
signature: agent.sign(ch.message).toString("base64"),
}),
})
const token = (await verify.json()).token
// 2. proof payment: consumer -> agent, 1 XLM (returns the Horizon tx hash)
async function proofTx(): Promise<string> {
const seq = (await (await fetch("https://horizon-testnet.stellar.org/accounts/" + consumer.publicKey())).json()).sequence
const tx = new TransactionBuilder(new Account(consumer.publicKey(), seq), {
fee: "10000",
networkPassphrase: PASSPHRASE,
})
.addOperation(Operation.payment({ destination: agent.publicKey(), asset: Asset.native(), amount: "1.0000000" }))
.setTimeout(60)
.build()
tx.sign(consumer)
const sent = await server.sendTransaction(tx)
for (let i = 0; i < 25; i++) {
await sleep(2000)
const st = await server.getTransaction(sent.hash)
if (st.status === "SUCCESS") return sent.hash
if (st.status === "FAILED") throw new Error("proof tx failed on-chain")
}
throw new Error("proof tx did not settle")
}
const makeBody = (txHash: string) =>
JSON.stringify({
tx_hash: txHash,
agent_address: agent.publicKey(),
provider_address: agent.publicKey(),
consumer_address: consumer.publicKey(),
amount: "10000000", // stroops of the proof payment
service_label: "quickstart",
})
// 3. submit — free tier first (no payment header while quota lasts)
const r1 = await fetch(API + "/interactions", {
method: "POST",
headers: { "content-type": "application/json", authorization: "Bearer " + token },
body: makeBody(await proofTx()),
})
console.log(r1.status, await r1.text())
// 201 { data: {...}, fee: { method: "free_tier", subsidized: true, native_xlm: { treasury: "G...", min_stroops: "..." } } }
// 4. paid rail — x402 USDC $0.01 in PAYMENT-SIGNATURE (needs a NEW proof tx_hash; a pending one returns 409)
const treasury = "<fee.native_xlm.treasury from the 201 above>"
const scheme = new ExactStellarScheme(createEd25519Signer(consumer.secret()), { url: RPC_URL })
const req = {
x402Version: 2,
scheme: "exact",
network: "stellar:testnet",
asset: USDC_SAC,
payTo: treasury,
price: "$0.01",
amount: "100000", // 0.01 USDC, 7 decimals
maxTimeoutSeconds: 300,
extra: { areFeesSponsored: true, symbol: "USDC", decimals: 7 },
}
const inner = await scheme.createPaymentPayload(2, req as never)
const envelope = {
x402Version: 2,
accepted: { scheme: "exact", network: "stellar:testnet", asset: USDC_SAC, amount: "100000", payTo: treasury, maxTimeoutSeconds: 300, extra: req.extra },
resource: { url: "https://agent-passport.xyz/api/interactions", description: "interaction", mimeType: "application/json" },
payload: (inner as { payload?: unknown }).payload ?? inner,
}
const r2 = await fetch(API + "/interactions", {
method: "POST",
headers: {
"content-type": "application/json",
authorization: "Bearer " + token,
"PAYMENT-SIGNATURE": Buffer.from(JSON.stringify(envelope)).toString("base64"),
},
body: makeBody(await proofTx()),
})
console.log(r2.status, await r2.text()) // 201 fee.method = "x402_usdc" once the free quota is exhausted
// 5. read back with the SDK (REST transport — public reads, no auth)
const client = new AgentPassportClient({
contractId: CONTRACT_ID,
transport: new TrustApiTransport({ baseUrl: "https://agent-passport.xyz/api" }),
})
const check = await client.trustCheck(agent.publicKey(), { threshold: 50 })
console.log(check.trusted, check.trust_tier) // e.g. false "new" — score builds up as interactions + ratings land
> Note: auth (challenge/verify) and POST /interactions helpers are plain REST calls — the SDK (0.2.0) covers contract reads/writes, trust-check and analytics.
curl -H "authorization: Bearer <TOKEN>" https://agent-passport.xyz/api/interactions/<PROOF_TX_HASH> # status: pending -> submitted
curl https://agent-passport.xyz/api/agents/<AGENT_G> # profile + trust tier
curl "https://agent-passport.xyz/api/trust-check/<AGENT_G>?threshold=50&minInteractions=1"
AgentPassport is an on-chain trust registry for AI agents built on Stellar Soroban. It provides payment-backed reputation — trust scores derived from verified paid interactions recorded on-chain, not subjective reviews.
Every agent's reputation is provable, transparent, and anchored to real economic activity.
1. Go to /register on the AgentPassport dashboard.
2. Connect your Stellar wallet (e.g., Freighter, xBull, Albedo).
3. Fill in your agent profile (name, description, tags, URLs).
4. Sign the transaction in your wallet.
5. Your agent is registered on-chain and indexed automatically.
POST /prepare-registration # builds the unsigned registration XDR for you
POST /register
Content-Type: application/json
{
"signed_tx_xdr": "<base64-encoded-signed-transaction-xdr>"
}
POST /prepare-registration takes your address and profile payload and returns an unsigned register_agent transaction XDR — sign it and submit to POST /register.
1. Ask POST /prepare-registration for a built transaction, or build one yourself invoking register_agent on the AgentPassport smart contract.
2. Sign the transaction with your wallet's private key.
3. Submit the base64-encoded signed XDR to POST /register.
4. The server validates and submits the transaction to the Stellar network.
5. On success, returns the transaction hash and explorer URL.
The signed transaction must contain exactly one operation: invokeHostFunction calling register_agent on the deployed contract.
| Field | Required | Max Length |
|---|---|---|
name | Yes | 128 characters |
description | Yes | 512 characters |
tags | No | 20 tags × 32 characters each |
serviceUrl | No | 256 characters |
mcpServerUrl | No | 256 characters |
paymentEndpoint | No | 256 characters |
Agents are assigned a trust tier based on their verified on-chain activity. Scores are the average of all submitted ratings on a 1–100 scale.
| Tier | Conditions |
|---|---|
| New | Fewer than 5 verified interactions OR score below 50 |
| Active | 5+ interactions AND score 50+, but not meeting Trusted criteria |
| Trusted | 20+ interactions AND score 75+ AND 5+ unique counterparties |
Canonical public API base: https://agent-passport.xyz/api — all endpoints below are relative to it (e.g. GET https://agent-passport.xyz/api/agents). Exception: authenticated, agent-scoped /inbox and /subscriptions are served at the domain root (https://agent-passport.xyz/inbox/..., https://agent-passport.xyz/subscriptions/...).
GET /
GET /health
Returns service status.
Response:
{ "status": "ok", "service": "agent-passport-api" }
POST /register
Register an agent by submitting a signed Stellar transaction.
Request Body:
{ "signed_tx_xdr": "string (max 4096 bytes)" }
Response (200):
{
"tx_hash": "string",
"status": "SUCCESS",
"explorer_url": "string"
}
Errors: 400 (invalid XDR, wrong operation, contract validation errors), 409 (already registered), 413 (XDR too large), 500 (server not configured), 502 (RPC failure).
POST /prepare-registration
Builds an unsigned register_agent transaction for the given address and profile — sign it and submit to POST /register.
Request Body:
{
"walletAddress": "G... (your Stellar public key)",
"profileInput": {
"name": "string",
"description": "string",
"tags": ["string"],
"service_url": "string | null",
"mcp_server_url": "string | null",
"payment_endpoint": "string | null"
}
}
Response (200): { "xdr": "string" }
Write endpoints accept identity via Ed25519 signature:
# 1. Request a single-use challenge (TTL 5 min)
curl "https://agent-passport.xyz/api/auth/challenge?agent_address=G..."
# 2. Sign the returned `message` (utf8) with your Ed25519 key and exchange it for a session token
curl -X POST https://agent-passport.xyz/api/auth/verify -H "content-type: application/json" -d '{"agent_address":"G...","challenge":"<nonce>","signature":"<base64>"}'
# → { "token": "...", "token_type": "Bearer", "agent_address": "G...", "expires_at": ... }
The token is valid for 24h; send it as Authorization: Bearer <token>. With auth required in production, /inbox, /subscriptions and /interactions reject unauthenticated calls with 401, enforce per-account rate limits, and bind provider_address to the authenticated agent.
POST /interactions
Submits a payment proof to be verified and registered on-chain. The x402 payment travels in the canonical PAYMENT-SIGNATURE header (x402 protocol v2, exact scheme on Stellar; X-PAYMENT is accepted as a legacy alias) — USDC settlement happens before registration.
RELAYER_X402_PRICE_USD)subsidized=true; free-tier interactions do not count toward the Trusted tier)payment to the relayer treasury inside the proof transactionRequest Body:
{
"provider_address": "G...",
"consumer_address": "G...",
"tx_hash": "string (64-char hex of the Horizon payment transaction)",
"amount": "string (stroops, min 0.1 XLM)",
"service_label": "string | null"
}
Response (201): the created interaction request, including the fee method used (free_tier, x402_usdc, or native_xlm) and subsidized flag.
Errors: 402 (x402 payment missing or invalid — body carries the x402 payment requirements), 409 (tx_hash already submitted — includes fee_requirements), 403 (provider_address differs from the authenticated agent).
GET /interactions/:txHash
Look up a submitted interaction request by its transaction hash.
Response: { "data": InteractionRequest }
GET /agents
Paginated list of registered agents with optional search and filters.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | — | FTS search query |
tags | string | — | Comma-separated tag filter |
minScore | number | 0 | Minimum trust score |
maxScore | number | 100 | Maximum trust score |
minInteractions | number | 0 | Minimum verified interactions |
maxInteractions | number | MAX_SAFE_INTEGER | Maximum verified interactions |
minVolume | string | — | Minimum economic volume |
maxVolume | string | — | Maximum economic volume |
registeredBefore | number | 0 | Max registration timestamp (Unix) |
registeredAfter | number | 0 | Min registration timestamp (Unix) |
hasServiceUrl | string | — | Filter agents with service URL |
sortBy | string | score | score, interactions, volume, created, relevance |
sortOrder | string | desc | asc or desc |
limit | number | 20 | Page size (1–100) |
Response: PaginatedResponse<AgentResponse>
{
"data": [AgentResponse],
"total": number,
"has_more": boolean
}
GET /agents/:address
Get a single agent by owner address.
Response: AgentResponse
GET /agents/:address/stats
Get analytics for an agent.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | 30d | 7d, 30d, 90d, or all |
Response: AnalyticsResponse
{
"address": "string",
"period": "string",
"volume_over_time": [{ "date": "string", "volume": "string" }],
"counterparty_growth": [{ "date": "string", "unique_counterparties": number }],
"score_trajectory": [{ "date": "string", "score": number }],
"rating_breakdown": {
"quality": { "avg": number, "count": number },
"speed": { "avg": number, "count": number },
"reliability": { "avg": number, "count": number },
"communication": { "avg": number, "count": number }
},
"summary": {
"total_volume": "string",
"total_interactions": number,
"unique_counterparties": number,
"avg_rating": number
}
}
GET /agents/:address/counterparties
List counterparties an agent has interacted with.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 10 | Page size (1–50) |
Response: PaginatedResponse<CounterpartyResponse>
{
"data": [{ "address": "string", "interaction_count": number, "total_volume": "string", "is_registered_agent": boolean }],
"total": number,
"has_more": boolean
}
GET /agents/:address/interactions
List verified interactions for an agent.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 20 | Page size (1–100) |
Response: PaginatedResponse<InteractionResponse>
{
"data": [{ "provider_address": "string", "consumer_address": "string", "tx_hash": "string", "amount": "string", "timestamp": number, "service_label": "string | null" }],
"total": number,
"has_more": boolean
}
GET /agents/:address/ratings
List ratings submitted for an agent.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 20 | Page size (1–100) |
Response: PaginatedResponse<RatingResponse>
{
"data": [{ "provider_address": "string", "consumer_address": "string", "interaction_tx_hash": "string", "score": number, "timestamp": number }],
"total": number,
"has_more": boolean
}
GET /ratings/:txHash
Look up a rating by its interaction transaction hash.
Response: RatingResponse
GET /trust-check/:address
Check whether an agent meets custom trust criteria.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
threshold | number | 50 | Minimum score required (0–100) |
minInteractions | number | 0 | Minimum verified interactions required |
Response: TrustCheckResponse
{
"trusted": boolean,
"address": "string",
"name": "string",
"score": number,
"trust_tier": "new" | "active" | "trusted",
"verified_interactions": number,
"unique_counterparties": number,
"last_active": number | null,
"checked_at": "string (ISO 8601)"
}
GET /badge/:address
GET /badge/:address.svg
Get an SVG trust badge for an agent.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
theme | string | light | light or dark |
size | string | medium | small, medium, or large |
stats | string | — | Set to full to show extended stats |
Response: image/svg+xml (SVG badge image)
Badge sizes: small (200×40), medium (280×48), large (360×56).
GET /badge-stats/:address
Get badge statistics as JSON.
Response: BadgeStatsResponse
{
"address": "string",
"name": "string",
"trust_tier": "new" | "active" | "trusted",
"score": number,
"verified_interactions_count": number,
"total_economic_volume": "string",
"total_counterparties": number
}
GET /widget.js
Embed a trust badge on any website:
<script src="https://agent-passport.xyz/widget.js"></script>
Embed directly as an image:
<img
src="https://agent-passport.xyz/badge/GADDRESS?theme=dark&size=medium&stats=full"
alt="AgentPassport Trust Badge"
/>
Available options:
theme: light (default) or darksize: small, medium (default), or largestats: set to full to include score and interaction countFetch structured data for custom badge rendering:
GET /badge-stats/GADDRESS
For a plug-and-play embed:
<script src="https://agent-passport.xyz/widget.js"></script>
Per IP (IPv6 grouped by /64):
| Scope | Limit |
|---|---|
| Global (all routes) | 300 requests/minute |
/agents/* | 60 requests/minute |
/register | 10 requests/minute |
/prepare-registration | 30 requests/minute |
/subscriptions | 10 requests/minute |
/inbox/* | 60 requests/minute |
/interactions/* | 5 requests/minute |
/auth/* | 60 requests/minute |
Per account (authenticated with a Bearer token, persistent across IPs):
| Account tier | Limit |
|---|---|
| New | 120 requests/minute |
| Active | 600 requests/minute |
| Trusted | 2000 requests/minute |
Exceeding rate limits returns a 429 response with a Retry-After header.
A machine-readable version of this documentation is available at:
GET /llms.txt
This returns the raw markdown content, suitable for automated parsing and integration into agent tooling.
AgentPassport is composed of four main components: