name: 8004stellar description: Use when building on Stellar/Soroban with ERC-8004 Agent Trust Protocol — agent identity registration, reputation feedback, validator endorsements.
ERC-8004: Agent Trust Protocol on Stellar
ERC-8004 establishes trust infrastructure for autonomous AI agents on Stellar/Soroban, enabling them to discover, identify, and evaluate other agents across organizational boundaries.
Companion skill:
/stellar-devfor Soroban transaction patterns, Freighter integration,nativeToScValreference, and testnet setup.
When to Use
- Registering an AI agent's identity on-chain so it becomes discoverable
- Building reputation systems for AI agents (feedback, scoring, trust thresholds)
- Verifying agent identity and reputation before interacting or paying
- Querying agent reputation and feedback history
- Implementing trust-based agent interactions with x402 payments
The Protocol Stack
Application Layer (Agent apps, marketplaces, 8004scan)
↓
Trust Layer (ERC-8004: identity + reputation + validation) ← this skill
↓
Payment Layer (x402: USDC micropayments) ← /x402stellar skill
↓
Communication Layer (A2A, MCP, HTTP)
ERC-8004 sits at the Trust Layer — you verify trust first, then enable payment, then communicate. Without trust, agents can't evaluate each other before transacting.
Contract Addresses
Stellar Testnet
| Contract | Address |
|---|---|
| Identity Registry | CDE3K4COIAGWNNJQQLL26SYI3KBJF5FUDHXG5FA6GYDJCG7T5V7FIWZH |
| Reputation Registry | CBZEAGIEI3HXMDRLF44KLQJQQOH6LCYWWSGJVSYQYQO2HQ6DDGZ7HT55 |
| Validation Registry | CC5USZRO26MOIAVNYTTJDS63C2OBBLREOAOET4CPF2EZWO3YFKLMO3SL |
Stellar Mainnet
| Contract | Address |
|---|---|
| Identity Registry | CBGPDCJIHQ32G42BE7F2CIT3YW6XRN5ED6GQJHCRZSNAYH6TGMCL6X35 |
| Reputation Registry | CBOIAIMMWAXI57OATLX6BWVDQLCC4YU55HV6MZXFRP6CBSGAMXSTEPPA |
| Validation Registry | CBT6WWEVEPT2UFGFGVJJ7ELYGLQAGRYSVGDTGMCJTRWXOH27MWUO7UJG |
Network Configuration
| Network | RPC URL | Passphrase |
|---|---|---|
| Testnet | https://soroban-testnet.stellar.org |
Test SDF Network ; September 2015 |
| Mainnet | https://mainnet.sorobanrpc.com |
Public Global Stellar Network ; September 2015 |
Prerequisites
npm install @trionlabs/stellar8004 @stellar/stellar-sdk
- Stellar keypair (G.../S... format) —
Keypair.random()for testnet,Keypair.fromSecret(secret)for existing wallets - Signer —
wrapBasicSigner(keypair, networkPassphrase)for scripts,FreighterSignerfor browser apps - Testnet: fund via
fundTestnet(address)from SDK or Friendbot - Companion skill
/stellar-devfor Soroban transaction fundamentals
Step 1: Prepare Your Agent Metadata
Metadata Format
Host as IPFS, HTTPS, or inline data URI — passed to register_with_uri():
{
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
"name": "My AI Agent",
"description": "What your agent does",
"image": "https://example.com/agent-logo.png",
"services": [
{ "name": "x402", "endpoint": "https://your-agent.example.com/task", "version": "1.0", "description": "Summarize documents for USDC", "inputExample": "{\"url\": \"https://example.com/doc.pdf\"}" },
{ "name": "mcp", "endpoint": "https://your-agent.example.com/mcp" },
{ "name": "a2a", "endpoint": "https://your-agent.example.com/.well-known/agent.json" }
],
"supportedTrust": ["reputation"],
"x402": true
}
Required fields:
type— must containeip-8004orregistration-v1name— non-empty string
Service Types
| Service | Purpose | Endpoint Format |
|---|---|---|
x402 |
HTTP API with USDC micropayments | https://agent.example.com/task |
mcp |
Model Context Protocol server | https://agent.example.com/mcp |
a2a |
Agent-to-Agent protocol | https://agent.example.com/.well-known/agent.json |
| custom | Any other protocol (e.g. rest, oasf, xmtp) |
Your URL |
Each service entry supports optional description (what the service does) and inputExample (example JSON input). These are displayed in the explorer's Try panel and help users understand your agent's capabilities.
Service types affect discoverability — the explorer filters by hasServices and x402.
Hosting Your Metadata
| Method | When to Use | Limit |
|---|---|---|
| Data URI | Simple agents, under 8KB | data:application/json;base64,... |
| IPFS | Immutable metadata | ipfs://Qm... |
| HTTPS | Mutable, updateable metadata | https://your-server.com/agent.json |
Using the SDK
import { buildMetadataJson, toDataUri, validateMetadataJson } from '@trionlabs/stellar8004';
const metadata = buildMetadataJson({
name: 'My AI Agent',
description: 'Summarizes documents and answers questions',
imageUrl: 'https://example.com/logo.png',
services: [
{ name: 'x402', endpoint: 'https://my-agent.example.com/task', version: '1.0', description: 'Summarize documents for USDC', inputExample: '{"url": "https://example.com/doc.pdf"}' },
{ name: 'mcp', endpoint: 'https://my-agent.example.com/mcp' },
],
supportedTrust: ['reputation'],
x402Enabled: true,
});
// Validate before registering (throws on invalid metadata)
validateMetadataJson(metadata);
// Convert to data URI (fails if > 8KB)
const dataUri = toDataUri(metadata);
Common Metadata Mistakes
| Mistake | Consequence |
|---|---|
Missing type field |
Indexer cannot categorize; agent may not appear in filtered searches |
Missing name |
Agent shows as unnamed in explorer |
| Data URI > 8KB | Transaction fails — use IPFS or HTTPS instead |
| Unreachable HTTPS URL | Indexer retries exhaust (5 attempts); agent stuck as unresolved |
Empty endpoint in service |
Service silently dropped by URI resolver |
Step 2: Register On-Chain
Using the SDK (recommended)
import { Keypair } from '@stellar/stellar-sdk';
import { createClients, TESTNET_CONFIG, wrapBasicSigner } from '@trionlabs/stellar8004';
// Create a signer from a Stellar secret key (S-format)
const keypair = Keypair.fromSecret(process.env.STELLAR_SECRET_KEY!);
const signer = wrapBasicSigner(keypair, TESTNET_CONFIG.networkPassphrase);
const { identity } = createClients(TESTNET_CONFIG, signer);
// Register with metadata URI (recommended)
const tx = await identity.register_with_uri({
caller: keypair.publicKey(),
agent_uri: dataUri, // from Step 1
});
const sent = await tx.signAndSend();
const agentId = sent.result; // u32 — save this, it's your agent's permanent ID
// Or register with URI + metadata in one call
const tx2 = await identity.register_full({
caller: keypair.publicKey(),
agent_uri: dataUri,
metadata: [{ key: 'category', value: new TextEncoder().encode('defi') }],
});
const sent2 = await tx2.signAndSend();
const agentId2 = sent2.result;
Without the SDK
All contract calls use buildAndSign() from references/build-and-sign.ts (simulate → assemble → sign → poll).
import * as StellarSdk from '@stellar/stellar-sdk';
// Register with URI
const args = [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }),
StellarSdk.nativeToScVal(dataUri, { type: 'string' }),
];
const { hash, result } = await buildAndSign('register_with_uri', IDENTITY_REGISTRY, args, signerAddress, signFn, rpc, networkPassphrase);
const agentId = StellarSdk.scValToNative(result) as number;
// Register minimal (no URI — agent won't be discoverable until URI is set)
const { result } = await buildAndSign('register', IDENTITY_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }),
], signerAddress, signFn, rpc, networkPassphrase);
// Register with URI + batch metadata
const metadataVec = StellarSdk.nativeToScVal(
[{ key: 'category', value: Buffer.from('defi') }],
{ type: { vec: { map: { key: 'string', value: 'bytes' } } } },
);
await buildAndSign('register_full', IDENTITY_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }),
StellarSdk.nativeToScVal(dataUri, { type: 'string' }),
metadataVec,
], signerAddress, signFn, rpc, networkPassphrase);
Updating Your Agent
// Update metadata URI (triggers re-indexing)
await buildAndSign('set_agent_uri', IDENTITY_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }), // caller
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal('ipfs://QmNewMetadata...', { type: 'string' }),
], signerAddress, signFn, rpc, networkPassphrase);
// Set on-chain key-value metadata
await buildAndSign('set_metadata', IDENTITY_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }),
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal('category', { type: 'string' }), // max 64 bytes
StellarSdk.nativeToScVal(Buffer.from('defi'), { type: 'bytes' }), // max 4KB
], signerAddress, signFn, rpc, networkPassphrase);
// Set agent wallet — BOTH caller and wallet must authorize (dual require_auth)
await buildAndSign('set_agent_wallet', IDENTITY_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }), // caller (owner)
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(walletAddress, { type: 'address' }), // new wallet
], signerAddress, signFn, rpc, networkPassphrase);
// Read helpers (simulate only, no signing needed)
// agent_uri(agent_id) → Result<String>
// get_metadata(agent_id, key) → Option<Bytes>
// get_agent_wallet(agent_id) → Option<Address>
// find_owner(agent_id) → Option<Address>
// agent_exists(agent_id) → bool
// total_agents() → u32
Extending TTL (Preventing Data Archival)
Soroban data expires if not extended. Call extend_ttl periodically to keep your agent's data alive:
await buildAndSign('extend_ttl', IDENTITY_REGISTRY, [
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
], signerAddress, signFn, rpc, networkPassphrase);
This extends TTL for: agent URI, wallet, all metadata keys + values, NFT owner, and owner balance. Without this, archived agents become invisible to on-chain reads.
Step 3: Get Indexed and Discovered
What Happens After Registration
1. You call register_with_uri() → Registered event emitted on-chain
2. Indexer detects event → creates agent record (resolve_uri_pending = true)
3. resolve-uris edge function runs every 30 seconds
4. Fetches your metadata URL → parses services, trust settings
5. Exponential backoff retry: 2, 4, 8, 16, 32 minutes (max 5 attempts)
6. On success: agent becomes searchable and visible in explorer
7. Your agent page: https://stellar8004.com/agents/{agentId}
Verify Your Registration
import { ExplorerClient } from '@trionlabs/stellar8004';
const explorer = new ExplorerClient(); // defaults to mainnet explorer
const agent = await explorer.getAgent(agentId);
console.log(agent.name, agent.services, agent.totalScore);
// Or check if your agents are listed
const myAgents = await explorer.getAgentsByAddress(myAddress);
Explorer API
| Endpoint | Query Params | Returns |
|---|---|---|
GET /api/v1/agents |
page, limit, trust, minScore, hasServices, x402, sortBy |
Paginated agent list with scores |
GET /api/v1/agents/{id} |
— | Single agent with full details |
GET /api/v1/agents/{id}/feedback |
page, limit, tag |
Paginated feedback entries |
GET /api/v1/search?q=... |
q (required), limit, trust, minScore |
Full-text search results |
GET /api/v1/stats |
— | Registry statistics (totalAgents, totalFeedbacks, etc.) |
GET /api/v1/accounts/{address}/agents |
page, limit |
Agents owned by address |
Base URL: https://stellar8004.com. Rate limit: 30 requests per IP.
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Agent not appearing | Metadata URL unreachable from indexer | Ensure URL is publicly accessible (no localhost, no private IPs) |
| Services not showing | Empty endpoint in service entry |
Verify each service has a non-empty endpoint |
| Still "indexing" after 30 min | All 5 retry attempts failed | Call set_agent_uri with corrected URL to trigger re-resolution |
| Agent shows as unnamed | Missing name in metadata |
Update metadata JSON and call set_agent_uri |
Step 4: Reputation — Give and Read Feedback
Why Reputation Matters
Reputation is the core of trust between agents:
- Trust before payment — check an agent's score before paying for its services via x402
- Sybil resistance —
get_summaryaccepts a filtered client list, letting you weight trusted sources - Discoverability — higher-scoring agents rank higher in the explorer (Total Score = Avg Feedback × 0.6 + Volume Factor × 0.2)
- Accountability — agents can respond to feedback on-chain via
append_response
Score Calculation
Total Score = (Average Feedback × 0.6) + (Volume Factor × 0.2)
Volume Factor = min(100, log(feedbackCount) / log(100) × 100)
More feedback from more unique clients = higher score = better discoverability.
give_feedback
Self-feedback is blocked. value is i128 (supports negative). value_decimals max 18.
// Create feedback hash (SHA-256)
const content = JSON.stringify({ quality: 'excellent', latency: '200ms' });
const hashBuf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(content));
const args = [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }), // caller (client)
StellarSdk.nativeToScVal(agentId, { type: 'u32' }), // agentId
StellarSdk.nativeToScVal(BigInt(85), { type: 'i128' }), // value (0-100 scale)
StellarSdk.nativeToScVal(0, { type: 'u32' }), // valueDecimals
StellarSdk.nativeToScVal('starred', { type: 'string' }), // tag1 (max 64 chars)
StellarSdk.nativeToScVal('', { type: 'string' }), // tag2
StellarSdk.nativeToScVal('https://agent.example.com', { type: 'string' }), // endpoint
StellarSdk.nativeToScVal('', { type: 'string' }), // feedbackUri
StellarSdk.nativeToScVal(new Uint8Array(hashBuf), { type: 'bytes' }), // feedbackHash
];
await buildAndSign('give_feedback', REPUTATION_REGISTRY, args, signerAddress, signFn, rpc, networkPassphrase);
Storage note: Only
value,value_decimals,is_revoked,tag1,tag2are stored on-chain. Theendpoint,feedback_uri, andfeedback_hashare emitted as event data only — to read them later, use the explorer API or parse events.
revoke_feedback
Revokes feedback you previously gave. Only the original feedback author can revoke.
await buildAndSign('revoke_feedback', REPUTATION_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }), // caller (original client)
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(BigInt(1), { type: 'u64' }), // feedbackIndex (1-indexed)
], signerAddress, signFn, rpc, networkPassphrase);
append_response
Callable by anyone — any authenticated account, not only the agent owner. The responder's address is recorded in the ResponseAppended event topic, so consumers must treat a response as coming from responder (not necessarily the agent owner) and must NOT assume a response proves agent-owner authorship. Multiple responses per feedback allowed.
const responseContent = JSON.stringify({ reply: 'Thank you for the feedback, we improved latency.' });
const responseHashBuf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(responseContent));
await buildAndSign('append_response', REPUTATION_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }), // caller (any authenticated account; recorded as responder)
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(clientAddress, { type: 'address' }), // feedback author
StellarSdk.nativeToScVal(BigInt(1), { type: 'u64' }), // feedbackIndex
StellarSdk.nativeToScVal('https://response.example.com', { type: 'string' }), // responseUri
StellarSdk.nativeToScVal(new Uint8Array(responseHashBuf), { type: 'bytes' }), // responseHash
], signerAddress, signFn, rpc, networkPassphrase);
read_feedback
Reads a single feedback entry. Returns stored fields only (not event-only fields like endpoint or feedbackUri).
const args = [
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(clientAddress, { type: 'address' }),
StellarSdk.nativeToScVal(BigInt(1), { type: 'u64' }), // feedbackIndex (1-indexed)
];
const { result } = await buildAndSign('read_feedback', REPUTATION_REGISTRY, args, signerAddress, signFn, rpc, networkPassphrase);
const feedback = StellarSdk.scValToNative(result);
// Returns FeedbackData:
// {
// value: i128, — the feedback score
// value_decimals: u32, — decimal precision
// is_revoked: bool, — whether revoked
// tag1: string, — primary tag
// tag2: string, — secondary tag
// }
get_summary — Running Aggregates
Returns aggregated reputation across multiple clients. No stored aggregates — the contract iterates all feedback on every call.
const args = [
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(clientAddresses, { type: { vec: 'address' } }), // max 5 clients
StellarSdk.nativeToScVal('starred', { type: 'string' }), // tag1 filter ('' = all)
StellarSdk.nativeToScVal('', { type: 'string' }), // tag2 filter ('' = all)
];
const { result } = await buildAndSign('get_summary', REPUTATION_REGISTRY, args, signerAddress, signFn, rpc, networkPassphrase);
const summary = StellarSdk.scValToNative(result);
// Returns SummaryResult:
// {
// count: u64, — number of matching feedback entries
// summary_value: i128, — WAD-normalized average score
// summary_value_decimals: u32, — mode decimal precision (most common)
// }
Performance characteristics:
MAX_SUMMARY_CLIENTS = 5— contract limits to 5 client addresses per call- Complexity: O(clients × feedbacks_per_client) — iterates all non-revoked feedback
- Filters by
tag1andtag2(empty string = no filter) - Revoked feedback is excluded automatically
- Sybil best practice: pass a curated list of trusted client addresses, not all clients
get_clients_paginated
Paginated list of addresses that have given feedback to an agent. Soroban per-tx budget caps arrays at ~100 entries.
const args = [
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(0, { type: 'u32' }), // start index
StellarSdk.nativeToScVal(20, { type: 'u32' }), // limit
];
const { result } = await buildAndSign('get_clients_paginated', REPUTATION_REGISTRY, args, signerAddress, signFn, rpc, networkPassphrase);
const clients = StellarSdk.scValToNative(result) as string[];
get_last_index and get_response_count
// Get the latest feedback index for a specific client
// Returns u64 — the highest feedback_index this client has given to this agent
const { result } = await buildAndSign('get_last_index', REPUTATION_REGISTRY, [
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(clientAddress, { type: 'address' }),
], signerAddress, signFn, rpc, networkPassphrase);
const lastIndex = StellarSdk.scValToNative(result) as number;
// Get the number of responses to a specific feedback entry
// Returns u32
const { result: countResult } = await buildAndSign('get_response_count', REPUTATION_REGISTRY, [
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(clientAddress, { type: 'address' }),
StellarSdk.nativeToScVal(BigInt(1), { type: 'u64' }), // feedbackIndex
], signerAddress, signFn, rpc, networkPassphrase);
const responseCount = StellarSdk.scValToNative(countResult) as number;
Reading Feedback via Explorer API (Off-Chain)
For bulk reads, the explorer API is more efficient than on-chain calls:
import { ExplorerClient } from '@trionlabs/stellar8004';
const explorer = new ExplorerClient();
const feedback = await explorer.getFeedback(agentId, { tag: 'starred', limit: 50 });
// Returns full feedback entries including event-only fields (endpoint, feedbackUri, feedbackHash)
Feedback Tags
| Tag | Measures | Example |
|---|---|---|
starred |
Quality rating (0-100) | 87/100 |
uptime |
Endpoint uptime % | 99.77% |
successRate |
Task success rate % | 89% |
responseTime |
Response time (ms) | 560ms |
reachable |
Endpoint reachable | 1/0 |
Tags are free-form strings (max 64 chars). The above are conventions, not enforced by the contract.
Step 5: Validation
Why Validation Matters
Reputation measures past performance. Validation attests to agent properties — identity verification, code audits, hardware guarantees.
| Model | Mechanism | Best For |
|---|---|---|
| Reputation-based | Client feedback via Step 4 | Low-stake, frequent interactions |
| Crypto-economic | Stake + slashing | Medium-stake financial operations |
| zkML | Zero-knowledge proofs | Privacy-preserving verification |
| TEE Attestation | Hardware isolation proofs | High-assurance environments |
validation_request
Submit a validation request for your agent to a specific validator.
// Generate unique request hash
const data = new TextEncoder().encode(
`${agentId}:${validatorAddr}:${Date.now()}:${crypto.randomUUID()}`
);
const requestHash = new Uint8Array(await crypto.subtle.digest('SHA-256', data));
await buildAndSign('validation_request', VALIDATION_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }), // caller
StellarSdk.nativeToScVal(validatorAddr, { type: 'address' }), // validator
StellarSdk.nativeToScVal(agentId, { type: 'u32' }), // agentId
StellarSdk.nativeToScVal('https://request.example.com', { type: 'string' }), // requestUri
StellarSdk.nativeToScVal(requestHash, { type: 'bytes' }), // requestHash
], signerAddress, signFn, rpc, networkPassphrase);
Or with the SDK:
import { generateRequestNonce } from '@trionlabs/stellar8004';
// generateRequestNonce is async (it awaits crypto.subtle.digest) — you MUST await it.
const requestHash = await generateRequestNonce(agentId, validatorAddr);
validation_response
Validator responds to a pending request.
const responseContent = JSON.stringify({ result: 'passed', details: 'code audit complete' });
const responseHashBuf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(responseContent));
await buildAndSign('validation_response', VALIDATION_REGISTRY, [
StellarSdk.nativeToScVal(validatorAddr, { type: 'address' }), // caller (validator)
StellarSdk.nativeToScVal(requestHash, { type: 'bytes' }), // requestHash
StellarSdk.nativeToScVal(85, { type: 'u32' }), // response: a 0-100 score (values > 100 revert with InvalidResponse)
StellarSdk.nativeToScVal('https://response.example.com', { type: 'string' }), // responseUri
StellarSdk.nativeToScVal(new Uint8Array(responseHashBuf), { type: 'bytes' }), // responseHash
StellarSdk.nativeToScVal('kyc', { type: 'string' }), // tag
], validatorAddr, validatorSignFn, rpc, networkPassphrase);
get_validation_status
const { result } = await buildAndSign('get_validation_status', VALIDATION_REGISTRY, [
StellarSdk.nativeToScVal(requestHash, { type: 'bytes' }),
], signerAddress, signFn, rpc, networkPassphrase);
const status = StellarSdk.scValToNative(result);
// Returns ValidationStatus:
// {
// validator_address: Address,
// agent_id: u32,
// response: u32, — a 0-100 score set by the validator (values > 100 revert).
// Use has_response (below), NOT response == 0, to detect "pending".
// response_hash: BytesN<32>,
// tag: string,
// last_update: u64, — ledger sequence
// has_response: bool,
// }
Storage note:
response_uriis event-only — use the indexer or parseValidationResponseevents to retrieve it.
get_summary (Validation)
const { result } = await buildAndSign('get_summary', VALIDATION_REGISTRY, [
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(validatorAddresses, { type: { vec: 'address' } }),
StellarSdk.nativeToScVal('kyc', { type: 'string' }), // tag filter ('' = all)
], signerAddress, signFn, rpc, networkPassphrase);
// Returns ValidationSummary with counts and response breakdown
Paginated Queries
// All validation request hashes for an agent
const { result } = await buildAndSign('get_agent_validations_paginated', VALIDATION_REGISTRY, [
StellarSdk.nativeToScVal(agentId, { type: 'u32' }),
StellarSdk.nativeToScVal(0, { type: 'u32' }), // start
StellarSdk.nativeToScVal(20, { type: 'u32' }), // limit
], signerAddress, signFn, rpc, networkPassphrase);
// All request hashes assigned to a validator
const { result: valResult } = await buildAndSign('get_validator_requests_paginated', VALIDATION_REGISTRY, [
StellarSdk.nativeToScVal(validatorAddr, { type: 'address' }),
StellarSdk.nativeToScVal(0, { type: 'u32' }),
StellarSdk.nativeToScVal(20, { type: 'u32' }),
], signerAddress, signFn, rpc, networkPassphrase);
// Check if a request exists
// request_exists(requestHash) → bool
Trust Verification Workflow
Full end-to-end: verify identity → check reputation → interact → pay via x402 → give feedback.
import * as StellarSdk from '@stellar/stellar-sdk';
import { ExplorerClient } from '@trionlabs/stellar8004';
async function verifyAndInteract(targetAgentId: number, minScore = 70) {
const explorer = new ExplorerClient();
// 1. Verify agent exists and get metadata
const agent = await explorer.getAgent(targetAgentId);
if (!agent) throw new Error('Agent not found');
// 2. Check reputation threshold
if (agent.feedbackCount > 0 && agent.avgScore < minScore) {
throw new Error(`Agent score ${agent.avgScore} below threshold ${minScore}`);
}
// 3. Find the service endpoint
const x402Service = agent.services?.find(s => s.name === 'x402');
if (!x402Service) throw new Error('Agent has no x402 endpoint');
// 4. Interact via x402 payment (see /x402stellar skill)
const response = await fetchPaid(x402Service.endpoint);
const result = await response.json();
// 5. Give feedback based on outcome
const feedbackValue = result.success ? BigInt(90) : BigInt(30);
const tag = result.success ? 'starred' : 'failed';
const hashBuf = await crypto.subtle.digest('SHA-256',
new TextEncoder().encode(JSON.stringify(result)));
await buildAndSign('give_feedback', REPUTATION_REGISTRY, [
StellarSdk.nativeToScVal(signerAddress, { type: 'address' }),
StellarSdk.nativeToScVal(targetAgentId, { type: 'u32' }),
StellarSdk.nativeToScVal(feedbackValue, { type: 'i128' }),
StellarSdk.nativeToScVal(0, { type: 'u32' }),
StellarSdk.nativeToScVal(tag, { type: 'string' }),
StellarSdk.nativeToScVal('', { type: 'string' }),
StellarSdk.nativeToScVal(x402Service.endpoint, { type: 'string' }),
StellarSdk.nativeToScVal('', { type: 'string' }),
StellarSdk.nativeToScVal(new Uint8Array(hashBuf), { type: 'bytes' }),
], signerAddress, signFn, rpc, networkPassphrase);
return result;
}
x402 Integration
Combine agent trust (/8004stellar) with payments (/x402stellar):
async function payTrustedAgent(agentId: number, serviceUrl: string) {
const explorer = new ExplorerClient();
// 1. Check reputation before paying
const agent = await explorer.getAgent(agentId);
if ((agent.avgScore ?? 0) < 80) {
throw new Error('Agent not trusted enough for payment');
}
// 2. Pay via x402 (see /x402stellar skill for full setup)
const response = await fetchPaid(serviceUrl);
// 3. Give feedback after service delivery
// (see give_feedback example above)
}
Pattern: verify trust → pay → give feedback. This creates a virtuous cycle — good agents earn reputation, bad agents lose it.
Storage vs Events
Understanding what's stored on-chain vs emitted as events is critical for reading data correctly.
What's Stored On-Chain (readable via contract calls)
| Registry | Stored Fields |
|---|---|
| Identity | Agent URI, wallet, metadata key-value pairs, NFT owner/balance |
| Reputation | FeedbackData { value, value_decimals, is_revoked, tag1, tag2 }, client list, last_index, response_count |
| Validation | ValidationStatus { validator_address, agent_id, response, response_hash, tag, last_update, has_response }, agent/validator index lists |
What's Event-Only (need indexer or event parsing)
| Registry | Event-Only Fields |
|---|---|
| Reputation | endpoint, feedback_uri, feedback_hash (in NewFeedback event) |
| Validation | response_uri (in ValidationResponse event) |
| Identity | All changes emit events, but data is also stored |
Why This Matters
read_feedback()returns{ value, value_decimals, is_revoked, tag1, tag2 }— not endpoint or feedbackUri- To get endpoint/URI fields, use the Explorer API (
explorer.getFeedback()) or parse events viarpc.getEvents() get_validation_status()returns everything exceptresponse_uri— parseValidationResponseevents for that
Error Handling
Transaction Errors
| Error | Cause | Fix |
|---|---|---|
tx_bad_seq |
Nonce mismatch — concurrent transactions | Retry with fresh account sequence |
TRY_AGAIN_LATER |
Network congestion | Wait a few seconds and retry |
Simulation failed |
Invalid args, insufficient auth, or contract error | Check error message for contract error code |
Transaction confirmation timed out |
Network delays | Check tx hash on explorer, may have succeeded |
Contract Error Codes
Identity Registry (11 codes): not owner or approved, URI not set, agent not found, metadata key too long, metadata value too long, too many metadata keys, reserved metadata key, empty value, no upgrade proposed, timelock not expired, upgrade already proposed
Reputation Registry (12 codes): self-feedback, feedback not found, invalid value decimals, not owner or approved (retained for ABI stability, unused), aggregate overflow, agent not found, empty value, value out of range, client addresses required, no upgrade proposed, timelock not expired, upgrade already proposed
Validation Registry (11 codes): not owner or approved, request not found, invalid response, request already exists, not designated validator, already responded (retained for ABI stability, unused), agent not found, counter overflow, no upgrade proposed, timelock not expired, upgrade already proposed
Use formatSorobanError(err) from the SDK to convert error codes to readable messages.
Technical Reference
All Public Methods
Identity Registry
| Method | Parameters | Returns |
|---|---|---|
register(caller) |
Address |
u32 (agentId) |
register_with_uri(caller, agent_uri) |
Address, String |
u32 |
register_full(caller, agent_uri, metadata) |
Address, String, Vec<MetadataEntry> |
u32 |
set_agent_uri(caller, agent_id, new_uri) |
Address, u32, String |
Result<()> |
agent_uri(agent_id) |
u32 |
Result<String> |
set_metadata(caller, agent_id, key, value) |
Address, u32, String, Bytes |
Result<()> |
get_metadata(agent_id, key) |
u32, String |
Option<Bytes> |
set_agent_wallet(caller, agent_id, new_wallet) |
Address, u32, Address |
Result<()> |
get_agent_wallet(agent_id) |
u32 |
Option<Address> |
unset_agent_wallet(caller, agent_id) |
Address, u32 |
Result<()> |
find_owner(agent_id) |
u32 |
Option<Address> |
agent_exists(agent_id) |
u32 |
bool |
is_authorized_or_owner(spender, agent_id) |
Address, u32 |
bool |
total_agents() |
— | u32 |
extend_ttl(agent_id) |
u32 |
— |
version() |
— | String |
Ownership (OZ 2-step): get_owner(), transfer_ownership(new_owner), accept_ownership(), renounce_ownership()
Upgrades (3-day timelock): propose_upgrade(new_wasm_hash), execute_upgrade(), cancel_upgrade(), pending_upgrade()
Reputation Registry
| Method | Parameters | Returns |
|---|---|---|
give_feedback(caller, agent_id, value, value_decimals, tag1, tag2, endpoint, feedback_uri, feedback_hash) |
Address, u32, i128, u32, String, String, String, String, BytesN<32> |
Result<()> |
revoke_feedback(caller, agent_id, feedback_index) |
Address, u32, u64 |
Result<()> |
append_response(caller, agent_id, client_address, feedback_index, response_uri, response_hash) |
Address, u32, Address, u64, String, BytesN<32> |
Result<()> |
read_feedback(agent_id, client_address, feedback_index) |
u32, Address, u64 |
Result<FeedbackData> |
get_summary(agent_id, client_addresses, tag1, tag2) |
u32, Vec<Address>, String, String |
Result<SummaryResult> |
get_clients_paginated(agent_id, start, limit) |
u32, u32, u32 |
Vec<Address> |
get_last_index(agent_id, client_address) |
u32, Address |
u64 |
get_response_count(agent_id, client_address, feedback_index) |
u32, Address, u64 |
u32 |
get_identity_registry() |
— | Address |
extend_ttl() |
— | — |
version() |
— | String |
Validation Registry
| Method | Parameters | Returns |
|---|---|---|
validation_request(caller, validator_address, agent_id, request_uri, request_hash) |
Address, Address, u32, String, BytesN<32> |
Result<()> |
validation_response(caller, request_hash, response, response_uri, response_hash, tag) |
Address, BytesN<32>, u32, String, BytesN<32>, String |
Result<()> |
get_validation_status(request_hash) |
BytesN<32> |
Result<ValidationStatus> |
request_exists(request_hash) |
BytesN<32> |
bool |
get_summary(agent_id, validator_addresses, tag) |
u32, Vec<Address>, String |
ValidationSummary |
get_agent_validations_paginated(agent_id, start, limit) |
u32, u32, u32 |
Vec<BytesN<32>> |
get_validator_requests_paginated(validator_address, start, limit) |
Address, u32, u32 |
Vec<BytesN<32>> |
get_identity_registry() |
— | Address |
extend_ttl() |
— | — |
version() |
— | String |
Contract Events
Identity Events
| Event | Topics | Data |
|---|---|---|
Registered |
[agentId: u32, owner: Address] |
agentUri: String |
UriUpdated |
[agentId: u32, updatedBy: Address] |
newUri: String |
MetadataSet |
[agentId: u32, key: String] |
value: Bytes |
Wallet changes emit
MetadataSetwithkey="agentWallet"— no separate WalletSet/WalletRemoved events.
Reputation Events
| Event | Topics | Data |
|---|---|---|
NewFeedback |
[agentId: u32, client: Address, tag1: String] |
feedbackIndex: u64, value: i128, valueDecimals: u32, tag2: String, endpoint: String, feedbackUri: String, feedbackHash: BytesN<32> |
FeedbackRevoked |
[agentId: u32, client: Address, feedbackIndex: u64] |
— |
ResponseAppended |
[agentId: u32, client: Address, responder: Address] |
feedbackIndex: u64, responseUri: String, responseHash: BytesN<32> |
Validation Events
| Event | Topics | Data |
|---|---|---|
ValidationRequest |
[validator: Address, agentId: u32, requestHash: BytesN<32>] |
requestUri: String |
ValidationResponse |
[validator: Address, agentId: u32, requestHash: BytesN<32>] |
response: u32, responseUri: String, responseHash: BytesN<32>, tag: String |
Querying Events
const rpc = new StellarSdk.rpc.Server('https://soroban-testnet.stellar.org');
const events = await rpc.getEvents({
startLedger: lastProcessedLedger,
filters: [{ type: 'contract', contractIds: [IDENTITY_REGISTRY] }],
limit: 100,
});
for (const event of events.events) {
const eventName = StellarSdk.scValToNative(event.topic[0]);
const firstTopic = StellarSdk.scValToNative(event.topic[1]);
// topic[0] = event name, topic[1..n] = indexed fields, event.value = data body
}
Additional Resources
Related Skills
- stellar-dev — General Stellar/Soroban development (required companion)
- x402stellar — x402 payment protocol on Stellar