| name | sati-sdk |
| description | Build with SATI (Solana Agent Trust Infrastructure) - on-chain agent identity, verifiable reputation, and blind feedback on Solana. Use when registering AI agents on-chain via Token-2022 NFTs, giving or searching feedback, querying agent reputation, building registration files (ERC-8004), encrypting attestation content, or integrating SATI into TypeScript/Node.js projects. Covers: CLI onboarding (create-sati-agent), agent registration, feedback (give/search), reputation summaries, agent search/discovery, validation attestations, EVM address linking, content encryption, and metadata uploading. Triggers on SATI, sati-sdk, create-sati-agent, agent registration solana, agent reputation, blind feedback, compressed attestation, Light Protocol attestation, ERC-8004 registration file, agent identity NFT, register agent CLI. |
SATI
Solana Agent Trust Infrastructure. Agents get Token-2022 NFT identities, accumulate verifiable feedback via ZK-compressed attestations (Light Protocol), and can be discovered on-chain.
Program ID (all networks): satiRkxEiwZ51cv8PRu8UMzuaqeaNU9jABo6oAFMsLe
Quick Start (CLI)
Fastest path - zero to registered agent in ~5 minutes:
npx create-sati-agent init
npx create-sati-agent publish
Mainnet:
npx create-sati-agent publish --network mainnet
All commands: init, publish, search, info [MINT], give-feedback, transfer <MINT>. All support --help, --json, --network devnet|mainnet.
agent-registration.json
The registration file follows the ERC-8004 Registration standard:
{
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
"name": "MyAgent",
"description": "AI assistant that does X for Y",
"image": "https://example.com/avatar.png",
"properties": {
"files": [{"uri": "https://example.com/avatar.png", "type": "image/png"}],
"category": "image"
},
"services": [
{
"name": "MCP",
"endpoint": "https://myagent.com/mcp",
"version": "2025-06-18",
"mcpTools": ["search", "summarize", "analyze"],
"mcpPrompts": ["data-analysis"],
"mcpResources": ["knowledge-base"]
},
{
"name": "A2A",
"endpoint": "https://myagent.com/.well-known/agent-card.json",
"a2aSkills": ["natural_language_processing/information_retrieval_synthesis/question_answering"]
}
],
"supportedTrust": ["reputation"],
"active": false,
"x402Support": false,
"registrations": []
}
Service types (see ERC-8004 best practices for detailed guidance):
MCP - Model Context Protocol. Fields: mcpTools (tool names as strings), mcpPrompts, mcpResources. The version field is the MCP spec version your server supports (e.g., "2025-06-18").
A2A - Agent-to-Agent. Fields: a2aSkills (OASF skill paths). Endpoint should point to your agent card JSON.
OASF - Open Agent Skills Framework. Fields: skills, domains.
ENS, DID, agentWallet - Identity services.
Note: When publishing via CLI (npx create-sati-agent publish), the CLI auto-discovers MCP tools by calling your MCP endpoint. Your MCP server must be running and reachable during publish. If your server requires auth, you'll see a non-blocking reachability warning - you can safely ignore it and list tools manually in the JSON.
Mainnet deployment flow
npx create-sati-agent init
npx create-sati-agent publish
npx create-sati-agent info <MINT> --network devnet
npx create-sati-agent publish --network mainnet
npx create-sati-agent transfer <MINT> \
--new-owner <SECURE_WALLET> --network mainnet
CLI feedback
npx create-sati-agent give-feedback \
--agent <MINT> --tag1 starred --value 85 --network mainnet
Feedback tag conventions:
| tag1 | value range | meaning |
|---|
starred | 0-100 | Overall rating |
reachable | 0 or 1 | Health check (1 = reachable) |
uptime | 0-100 | Uptime percentage |
responseTime | ms | Latency in milliseconds |
successRate | 0-100 | Success percentage |
Monitoring agent health
Automate health checks with a cron job or scheduled task:
curl -sf https://myagent.com/mcp > /dev/null && \
npx create-sati-agent give-feedback --agent <MINT> --tag1 reachable --value 1 --network mainnet || \
npx create-sati-agent give-feedback --agent <MINT> --tag1 reachable --value 0 --network mainnet
Reputation badge
Add a reputation badge to your README:

Or link to your dashboard page:
[Reputation](https://sati.cascade.fyi/agent/<YOUR_MINT>)
SDK (Programmatic)
@cascade-fyi/sati-sdk is the primary SDK for all SATI integrations.
Building a read-only integration? For explorers, dashboards, and data ingestion, the REST API requires no wallet or Solana dependencies. Use the SDK only when you need to write on-chain (register agents, give feedback, publish scores).
npm install @cascade-fyi/sati-sdk
npm install @solana/kit @solana-program/token-2022
Initialize
import { Sati, createSatiUploader, address } from "@cascade-fyi/sati-sdk";
import { createKeyPairSignerFromBytes } from "@solana/kit";
const sati = new Sati({ network: "mainnet" });
Load a wallet:
import { readFileSync } from "node:fs";
const bytes = new Uint8Array(JSON.parse(readFileSync("wallet.json", "utf8")));
const payer = await createKeyPairSignerFromBytes(bytes);
1. Register an Agent
Quick (fluent builder)
const builder = sati.createAgentBuilder("MyAgent", "AI assistant", "https://example.com/avatar.png");
builder
.setMCP("https://mcp.example.com", "2025-06-18", { tools: ["search"] })
.setA2A("https://a2a.example.com/.well-known/agent-card.json")
.setX402Support(true)
.setActive(true);
const result = await builder.register({
payer,
uploader: createSatiUploader(),
});
Direct
import { buildRegistrationFile, createSatiUploader } from "@cascade-fyi/sati-sdk";
const regFile = buildRegistrationFile({
name: "MyAgent",
description: "AI assistant",
image: "https://example.com/avatar.png",
services: [{ name: "MCP", endpoint: "https://mcp.example.com" }],
active: true,
});
const uploader = createSatiUploader();
const uri = await uploader.upload(regFile);
const result = await sati.registerAgent({
payer,
name: "MyAgent",
uri,
nonTransferable: false,
});
Uploaders: createSatiUploader() (zero-config, uses hosted IPFS via sati.cascade.fyi) or createPinataUploader(jwt).
2. Give Feedback
Public feedback (simple)
giveFeedback uses the FeedbackPublicV1 schema (CounterpartySigned mode) - the reviewer signs and submits in one call. No agent co-signature required.
import { Outcome } from "@cascade-fyi/sati-sdk";
const { signature, attestationAddress } = await sati.giveFeedback({
payer,
agentMint: address("Agent..."),
outcome: Outcome.Positive,
value: 87,
valueDecimals: 0,
tag1: "starred",
tag2: "chat",
message: "Great response time",
endpoint: "https://agent.example",
taskRef: txHashBytes,
});
x402 payment linking: The taskRef field accepts a 32-byte reference to link feedback to a specific transaction. x402 integration details (converting tx signatures to 32-byte refs, querying feedback by payment) are under active development.
Blind feedback (dual-signature)
For proof-of-participation, use the FeedbackV1 schema (DualSignature mode). The agent signs a blind commitment before knowing the outcome. Use the lower-level createFeedback() method with both agentSignature and counterpartyMessage. See the specification for the full blind feedback flow.
Note: For most integrations, FeedbackPublicV1 (single-signer via giveFeedback) is sufficient. Blind feedback requires agent-side signing integration and is primarily for proof-of-participation use cases where you need cryptographic evidence that the agent participated in the interaction.
Browser wallet flow (two-step)
The platform server prepares a SIWS (Sign In With Solana) message, the user signs it in their browser wallet, and the platform submits the transaction.
Uses @solana/wallet-adapter-react (works with Phantom, Solflare, Backpack, and any wallet implementing the Wallet Standard signMessage feature).
npm install @solana/wallet-adapter-react @solana/wallet-adapter-wallets @solana/wallet-adapter-react-ui
Server (API route):
import { Sati, Outcome, address, bytesToHex, hexToBytes } from "@cascade-fyi/sati-sdk";
const sati = new Sati({ network: "mainnet" });
async function handlePrepare(req) {
const { walletAddress, agentMint, value, tag1, outcome } = req.body;
const prepared = await sati.prepareFeedback({
counterparty: address(walletAddress),
agentMint: address(agentMint),
outcome: outcome ?? Outcome.Positive,
value,
tag1,
});
await cache.set(`feedback:${walletAddress}:${agentMint}`, prepared);
return { messageHex: bytesToHex(prepared.messageBytes) };
}
async function handleSubmit(req) {
const { walletAddress, agentMint, signatureHex } = req.body;
const prepared = await cache.get(`feedback:${walletAddress}:${agentMint}`);
const result = await sati.submitPreparedFeedback({
payer: platformPayer,
prepared,
counterpartySignature: hexToBytes(signatureHex),
});
return { signature: result.signature, attestationAddress: result.attestationAddress };
}
Frontend (React component):
import { useWallet } from "@solana/wallet-adapter-react";
import { hexToBytes, bytesToHex } from "@cascade-fyi/sati-sdk";
function FeedbackButton({ agentMint }: { agentMint: string }) {
const { publicKey, signMessage, connected } = useWallet();
async function handleFeedback() {
if (!publicKey || !signMessage) return;
const { messageHex } = await fetch("/api/prepare-feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
walletAddress: publicKey.toBase58(),
agentMint,
value: 85,
tag1: "starred",
}),
}).then((r) => r.json());
const messageBytes = hexToBytes(messageHex);
const signature = await signMessage(messageBytes);
await fetch("/api/submit-feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
walletAddress: publicKey.toBase58(),
agentMint,
signatureHex: bytesToHex(signature),
}),
});
}
return (
<button onClick={handleFeedback} disabled={!connected}>
Rate Agent
</button>
);
}
Note: signMessage is undefined if the connected wallet doesn't support message signing. Always check signMessage before calling it. PreparedFeedbackData contains multiple Uint8Array fields (messageBytes, taskRef, dataHash, content). If you need to serialize the entire object to JSON (e.g. for a stateless API), convert all Uint8Array fields with bytesToHex() and restore with hexToBytes(). The recommended pattern above avoids this by keeping prepared server-side.
3. Search Feedback
searchFeedback queries only the FeedbackPublicV1 schema. Use searchAllFeedback to query both FeedbackPublicV1 and FeedbackV1 (blind) schemas.
const feedbacks = await sati.searchFeedback({
agentMint: address("Agent..."),
tag1: "starred",
minValue: 70,
outcome: Outcome.Positive,
includeTxHash: true,
});
const allPublic = await sati.searchFeedback({});
const combined = await sati.searchAllFeedback({
agentMint: address("Agent..."),
});
To distinguish blind (FeedbackV1) from public (FeedbackPublicV1) in raw results, compare attestation.sasSchema against sati.feedbackSchema vs sati.feedbackPublicSchema.
Bulk ingestion (for indexers/scoring providers):
import { parseFeedbackContent } from "@cascade-fyi/sati-sdk";
for await (const page of sati.listAllFeedbacks({ agentMint: address("Agent...") })) {
for (const item of page.items) {
const parsed = parseFeedbackContent(item.data.content, item.data.contentType);
}
}
for await (const page of sati.listAllFeedbacks()) { }
Note: searchFeedback/searchAllFeedback return ParsedFeedback[] (fully parsed). listAllFeedbacks returns raw ParsedAttestation pages where content is still bytes - use parseFeedbackContent(item.data.content, item.data.contentType) to extract fields. createdAt timestamps in ParsedFeedback are approximate - derived from Solana slot numbers using ~400ms/slot estimate.
Incremental sync (scoring providers): There is no sinceSlot filter - Photon RPC does not support slot-range queries on compressed accounts. For incremental updates, track item.raw.slotCreated locally and skip items below your last-processed slot on each full fetch. At current volumes this is efficient; for higher scale, use a Solana transaction log indexer (Helius webhooks, Yellowstone gRPC) to stream new attestation events.
4. Reputation Summary
const summary = await sati.getReputationSummary(address("Agent..."));
const filtered = await sati.getReputationSummary(address("Agent..."), "starred", "chat");
Note: getReputationSummary queries both FeedbackPublicV1 and FeedbackV1 schemas. In the SDK, count only includes entries with a value field (entries without value are excluded from both count and average). The REST API differs: its count includes all feedback entries regardless of value, while summaryValue averages only entries that have value set. The REST API returns integer summaryValue/summaryValueDecimals instead of the SDK's float averageValue.
5. Agent Discovery
const agent = await sati.loadAgent(address("Mint..."));
const regFile = await fetchRegistrationFile(agent.uri);
const agents = await sati.loadAgents([mint1, mint2, mint3]);
const first = await sati.getAgentByMemberNumber(1n);
const results = await sati.searchAgents({
endpointTypes: ["MCP"],
active: true,
includeFeedbackStats: true,
limit: 50,
});
const page = await sati.listAllAgents({ limit: 20, offset: 0, order: "newest" });
const myAgents = await sati.listAgentsByOwner(address("Owner..."));
const stats = await sati.getRegistryStats();
6. Update Agent Metadata
builder.updateInfo({ description: "Updated description" });
builder.setMCP("https://new-mcp.example.com");
await builder.update({ payer, owner: ownerKeypair, uploader: createSatiUploader() });
await sati.updateAgentMetadata({
payer,
owner: ownerKeypair,
mint: address("Mint..."),
updates: { name: "NewName", uri: "ipfs://Qm..." },
});
7. Link EVM Address
Cross-chain identity linking via secp256k1 signature:
await sati.linkEvmAddress({
payer,
agentMint: address("Mint..."),
evmAddress: "0x1234...abcd",
chainId: "eip155:8453",
signature: secp256k1Sig,
recoveryId: 0,
});
Note: EVM links are stored as Anchor events only (not in on-chain accounts). There is no SDK query method to read past links - you need a Solana transaction log indexer (Helius, Yellowstone) to retrieve them.
8. Content Encryption
X25519-XChaCha20-Poly1305 for private feedback:
import {
deriveEncryptionKeypair,
encryptContent,
decryptContent,
serializeEncryptedPayload,
deserializeEncryptedPayload,
} from "@cascade-fyi/sati-sdk";
const encKeys = deriveEncryptionKeypair(ed25519PrivateKeyBytes);
const encrypted = encryptContent(plaintext, recipientX25519PublicKey);
const bytes = serializeEncryptedPayload(encrypted);
const decrypted = decryptContent(deserializeEncryptedPayload(bytes), recipientPrivateKey);
9. Registration File (ERC-8004)
import {
buildRegistrationFile,
validateRegistrationFile,
fetchRegistrationFile,
getImageUrl,
} from "@cascade-fyi/sati-sdk";
const result = validateRegistrationFile(untrustedData);
if (!result.ok) console.error(result.errors);
const regFile = await fetchRegistrationFile("ipfs://Qm...");
const imageUrl = getImageUrl(regFile);
See the ERC-8004 registration best practices for guidance on name, image, description, and services.
10. Reputation Scoring (on-chain)
For scoring providers publishing computed scores back on-chain (ReputationScoreV3 schema):
import { ContentType, parseReputationScoreContent } from "@cascade-fyi/sati-sdk";
await sati.updateReputationScore({
payer,
provider: providerKeypair,
sasSchema: sati.reputationScoreSchema,
satiCredential: sati.credential,
agentMint: address("Agent..."),
outcome: Outcome.Positive,
contentType: ContentType.JSON,
content: new TextEncoder().encode(JSON.stringify({ score: 85, factors: { ... } })),
});
const scores = await sati.listReputationScores(
address("Agent..."),
sati.reputationScoreSchema,
);
for (const score of scores) {
const parsed = parseReputationScoreContent(score.content, score.contentType);
}
const score = await sati.getReputationScore(
address("Provider..."),
address("Agent..."),
sati.credential,
sati.reputationScoreSchema,
);
Platform integration notes
Ownership model: registerAgent({ payer, owner }) - the payer pays gas, the owner receives the NFT. A platform can register agents on behalf of operators. Only the owner can update metadata. Reputation stays with the mint address (portable across owners).
Outcome enum values: Negative = 0, Neutral = 1, Positive = 2. Use getOutcomeLabel(outcome) for display strings.
REST API
The dashboard at sati.cascade.fyi exposes a public REST API. See the REST API reference for full endpoint documentation. Key endpoints:
GET /api/agents - list/search agents (supports name, owner, endpointTypes, order, pagination)
GET /api/agents/:mint - single agent with reputation summary
GET /api/feedback/:mint - feedback for an agent (paginated with limit/offset)
GET /api/feedback - global feedback across all agents (paginated)
GET /api/reputation/:mint - reputation summary with tag/reviewer filters
GET /api/stats - registry statistics (totalAgents, groupMint, etc.)
GET /api/scores/:mint - reputation scores from scoring providers (ReputationScoreV3)
GET /api/badge/:mint - SVG reputation badge for README embedding
POST /api/feedback - submit feedback without a wallet (server acts as counterparty, rate limited per IP)
The agents list supports includeReputation=true to get reputation inline per agent (slower but avoids N+1 requests). Filter params like endpointTypes are case-sensitive (use MCP, not mcp).
SDK ↔ REST API field mapping: counterparty (SDK) = clientAddress (REST). averageValue (SDK, float) = summaryValue/summaryValueDecimals (REST, integer). Outcome: Outcome.Positive = 2, Outcome.Neutral = 1, Outcome.Negative = 0. Use getOutcomeLabel(outcome) in SDK for display strings.
Note: EVM address links (from linkEvmAddress) are not queryable via REST API - they are stored as Anchor events only. Retrieving them requires a Solana transaction log indexer (Helius webhooks, Yellowstone gRPC).
Configuration
const sati = new Sati({
network: "mainnet",
rpcUrl: "https://...",
photonRpcUrl: "https://...",
onWarning: (w) => console.warn(w.code, w.message),
feedbackCacheTtlMs: 30_000,
transactionConfig: {
priorityFeeMicroLamports: 50_000,
computeUnitLimit: 400_000,
maxRetries: 2,
},
});
RPC endpoints: By default, the SDK routes all RPC calls through hosted proxies at sati.cascade.fyi (backed by Helius), rate-limited to ~120 req/min per IP. For production workloads, provide your own Helius or Triton RPC URLs via rpcUrl and photonRpcUrl to get higher limits.
Key Types
| Type | Description |
|---|
AgentIdentity | On-chain agent: mint, owner, name, uri, memberNumber (bigint), nonTransferable, additionalMetadata |
RegistrationFile | ERC-8004 metadata with services, trust mechanisms |
GiveFeedbackParams | Simplified feedback input (FeedbackPublicV1) |
ParsedFeedback | Feedback with value, tags, message, createdAt, counterparty |
FeedbackContent | Raw content: value, valueDecimals, tag1, tag2, m (message), endpoint, reviewer, feedbackURI, feedbackHash |
ReputationSummary | Aggregated count + averageValue |
AgentSearchResult | Identity + registrationFile + optional feedbackStats |
Outcome | Enum: Positive (2), Negative (0), Neutral (1) |
MetadataUploader | Interface for pluggable storage (IPFS, Arweave, etc.) |
Error Handling
import { SatiError, DuplicateAttestationError, AgentNotFoundError } from "@cascade-fyi/sati-sdk";
try {
await sati.giveFeedback(params);
} catch (e) {
if (e instanceof DuplicateAttestationError) {
}
}
Costs
| Operation | Cost |
|---|
| Agent registration | ~0.003 SOL |
| Agent transfer | ~0.0005 SOL |
| Feedback attestation | ~0.00001 SOL (compressed) |
| Reputation score (ReputationScoreV3) | ~0.002 SOL (regular SAS, not compressed) |
| Devnet | Free (auto-funded faucet) |
Common Issues
- Blockhash expired - Solana transactions must land within ~60 seconds. Retry the command/call.
- Insufficient funds (mainnet) - Send ~0.01 SOL to your wallet address. CLI shows the address on failure.
- Permission denied on update - Wrong keypair. Use
--keypair /path/to/original.json with the CLI, or ensure the correct owner KeyPairSigner in SDK.
- Feedback schema not deployed - Make sure you're on the right network. Schemas are deployed on both devnet and mainnet.
- Rate limited (429) - The hosted RPC proxies are rate-limited to ~120 req/min per IP. For production, provide your own RPC via
rpcUrl and photonRpcUrl.