sati-sdk
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.
git clone https://github.com/cascade-protocol/sati
git clone --depth=1 https://github.com/cascade-protocol/sati ~/.claude/skills/cascade-protocol-sati-sati-sdk
SKILL.md- makes HTTP requests (curl)
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 # Creates agent-registration.json + keypair # Edit agent-registration.json with your agent details npx create-sati-agent publish # Publishes to devnet (free, auto-funded)
Mainnet:
npx create-sati-agent publish --network mainnet # ~0.003 SOL
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):
- Model Context Protocol. Fields:MCP
(tool names as strings),mcpTools
,mcpPrompts
. ThemcpResources
field is the MCP spec version your server supports (e.g.,version
)."2025-06-18"
- Agent-to-Agent. Fields:A2A
(OASF skill paths). Endpoint should point to your agent card JSON.a2aSkills
- Open Agent Skills Framework. Fields:OASF
,skills
.domains
,ENS
,DID
- Identity services.agentWallet
Note: When publishing via CLI (
), 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.npx create-sati-agent publish
Mainnet deployment flow
npx create-sati-agent init # 1. Create template + keypair npx create-sati-agent publish # 2. Test on devnet (free, default) npx create-sati-agent info <MINT> --network devnet # 3. Verify npx create-sati-agent publish --network mainnet # 4. Go live (~0.003 SOL) npx create-sati-agent transfer <MINT> \ --new-owner <SECURE_WALLET> --network mainnet # 5. Move to hardware wallet
CLI feedback
npx create-sati-agent give-feedback \ --agent <MINT> --tag1 starred --value 85 --network mainnet
Feedback tag conventions:
| tag1 | value range | meaning |
|---|---|---|
| 0-100 | Overall rating |
| 0 or 1 | Health check (1 = reachable) |
| 0-100 | Uptime percentage |
| ms | Latency in milliseconds |
| 0-100 | Success percentage |
Monitoring agent health
Automate health checks with a cron job or scheduled task:
# Check if endpoint is reachable and report to SATI 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 # Peer deps: 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" }); // Options: network, rpcUrl, wsUrl, photonRpcUrl, onWarning, transactionConfig, feedbackCacheTtlMs
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(), // Zero-config IPFS upload }); // result.mint - agent NFT address, result.memberNumber, result.signature
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, // default: false. Set true for soulbound (non-transferable) agents. });
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, // Reviewer wallet (pays + signs) agentMint: address("Agent..."), // Agent to review outcome: Outcome.Positive, // Positive | Negative | Neutral (default: Neutral) value: 87, // Numeric score (optional) valueDecimals: 0, // Decimal places for value tag1: "starred", // Primary dimension tag2: "chat", // Secondary dimension (optional) message: "Great response time", // Human-readable (optional) endpoint: "https://agent.example", // Endpoint reviewed (optional) taskRef: txHashBytes, // 32-byte task reference (optional, e.g. payment tx hash) });
x402 payment linking: The
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.taskRef
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,
(single-signer viaFeedbackPublicV1) 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.giveFeedback
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" }); // POST /api/prepare-feedback 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, }); // Store `prepared` server-side (e.g. in session or cache keyed by walletAddress + agentMint) await cache.set(`feedback:${walletAddress}:${agentMint}`, prepared); // Only send the SIWS message bytes to the frontend return { messageHex: bytesToHex(prepared.messageBytes) }; } // POST /api/submit-feedback 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; // 1. Server prepares the SIWS message 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()); // 2. User signs with wallet (Phantom/Solflare popup) const messageBytes = hexToBytes(messageHex); const signature = await signMessage(messageBytes); // Returns Uint8Array (64-byte Ed25519) // 3. Server submits the transaction 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:
issignMessageif the connected wallet doesn't support message signing. Always checkundefinedbefore calling it.signMessagecontains multiplePreparedFeedbackDatafields (Uint8Array,messageBytes,taskRef,dataHash). If you need to serialize the entire object to JSON (e.g. for a stateless API), convert allcontentfields withUint8Arrayand restore withbytesToHex(). The recommended pattern above avoids this by keepinghexToBytes()server-side.prepared
3. Search Feedback
searchFeedback queries only the FeedbackPublicV1 schema. Use searchAllFeedback to query both FeedbackPublicV1 and FeedbackV1 (blind) schemas.
// Search FeedbackPublicV1 for a specific agent const feedbacks = await sati.searchFeedback({ agentMint: address("Agent..."), tag1: "starred", minValue: 70, outcome: Outcome.Positive, // Filter by outcome (optional) includeTxHash: true, }); // Returns: ParsedFeedback[] with compressedAddress, outcome, value, tag1, tag2, message, createdAt // Search FeedbackPublicV1 across all agents (omit agentMint) const allPublic = await sati.searchFeedback({}); // Search BOTH schemas (FeedbackPublicV1 + FeedbackV1) 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"; // Auto-paginating async iterator across both schemas for await (const page of sati.listAllFeedbacks({ agentMint: address("Agent...") })) { for (const item of page.items) { // item.data: { taskRef, agentMint, counterparty, dataHash, outcome, contentType, content } // item.raw.slotCreated (bigint) for on-chain slot // item.address is Uint8Array - decode with getAddressDecoder() from @solana/kit: // import { getAddressDecoder } from "@solana/kit"; // const [address] = getAddressDecoder().read(item.address, 0); const parsed = parseFeedbackContent(item.data.content, item.data.contentType); // parsed: { value, valueDecimals, tag1, tag2, m (message), endpoint, reviewer, feedbackURI, feedbackHash } } } // Omit agentMint to iterate ALL feedback across all agents 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...")); // { count: 42, averageValue: 85.3 } // Filter by tags: 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
// Load single agent (on-chain data only - no description, image, or services) const agent = await sati.loadAgent(address("Mint...")); // AgentIdentity: { mint, owner, name, uri, memberNumber, nonTransferable } // For rich metadata (description, image, services, active), fetch the registration file: const regFile = await fetchRegistrationFile(agent.uri); // regFile: { name, description, image, services, active, x402Support, supportedTrust, ... } // Load multiple agents in batch (single batched RPC call) const agents = await sati.loadAgents([mint1, mint2, mint3]); // Returns: (AgentIdentity | null)[] - null for invalid/missing mints // Get agent by member number (1-indexed) const first = await sati.getAgentByMemberNumber(1n); // Search agents with filters const results = await sati.searchAgents({ endpointTypes: ["MCP"], active: true, includeFeedbackStats: true, limit: 50, }); // AgentSearchResult[]: { identity, registrationFile, feedbackStats } // List all agents with pagination (lighter than searchAgents - no registration file fetch) // Default limit: 100, offset: 0, order: "newest" const page = await sati.listAllAgents({ limit: 20, offset: 0, order: "newest" }); // { agents: AgentIdentity[], totalAgents: bigint } // List by owner const myAgents = await sati.listAgentsByOwner(address("Owner...")); // Registry stats const stats = await sati.getRegistryStats(); // { totalAgents, groupMint, authority, isImmutable }
6. Update Agent Metadata
// Via builder builder.updateInfo({ description: "Updated description" }); builder.setMCP("https://new-mcp.example.com"); await builder.update({ payer, owner: ownerKeypair, uploader: createSatiUploader() }); // Direct 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", // Base signature: secp256k1Sig, // 64 bytes: r || s 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"; // Derive from Ed25519 keypair const encKeys = deriveEncryptionKeypair(ed25519PrivateKeyBytes); const encrypted = encryptContent(plaintext, recipientX25519PublicKey); const bytes = serializeEncryptedPayload(encrypted); // ... store bytes as attestation content ... const decrypted = decryptContent(deserializeEncryptedPayload(bytes), recipientPrivateKey);
9. Registration File (ERC-8004)
import { buildRegistrationFile, validateRegistrationFile, fetchRegistrationFile, getImageUrl, } from "@cascade-fyi/sati-sdk"; // Validate untrusted data const result = validateRegistrationFile(untrustedData); if (!result.ok) console.error(result.errors); // Fetch from URI (IPFS/HTTP) 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"; // Publish/update a score (idempotent - closes existing + creates new in one tx) await sati.updateReputationScore({ payer, provider: providerKeypair, // Scoring provider's KeyPairSigner sasSchema: sati.reputationScoreSchema, satiCredential: sati.credential, agentMint: address("Agent..."), outcome: Outcome.Positive, contentType: ContentType.JSON, content: new TextEncoder().encode(JSON.stringify({ score: 85, factors: { ... } })), }); // Read existing scores for an agent const scores = await sati.listReputationScores( address("Agent..."), sati.reputationScoreSchema, ); for (const score of scores) { const parsed = parseReputationScoreContent(score.content, score.contentType); // parsed: { score, factors, ... } } // Get a specific provider's score 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:
- list/search agents (supportsGET /api/agents
,name
,owner
,endpointTypes
, pagination)order
- single agent with reputation summaryGET /api/agents/:mint
- feedback for an agent (paginated withGET /api/feedback/:mint
/limit
)offset
- global feedback across all agents (paginated)GET /api/feedback
- reputation summary with tag/reviewer filtersGET /api/reputation/:mint
- registry statistics (GET /api/stats
,totalAgents
, etc.)groupMint
- reputation scores from scoring providers (ReputationScoreV3)GET /api/scores/:mint
- SVG reputation badge for README embeddingGET /api/badge/:mint
- submit feedback without a wallet (server acts as counterparty, rate limited per IP)POST /api/feedback
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:
(SDK) =counterparty(REST).clientAddress(SDK, float) =averageValue/summaryValue(REST, integer). Outcome:summaryValueDecimals=Outcome.Positive,2=Outcome.Neutral,1=Outcome.Negative. Use0in SDK for display strings.getOutcomeLabel(outcome)
Note: EVM address links (from
) 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).linkEvmAddress
Configuration
const sati = new Sati({ network: "mainnet", // "mainnet" | "devnet" | "localnet" rpcUrl: "https://...", // Custom Solana RPC (optional) photonRpcUrl: "https://...", // Photon/Helius RPC for Light Protocol queries (optional) onWarning: (w) => console.warn(w.code, w.message), feedbackCacheTtlMs: 30_000, // Cache TTL (default 30s, 0 to disable) transactionConfig: { priorityFeeMicroLamports: 50_000, // Default on mainnet computeUnitLimit: 400_000, maxRetries: 2, // Blockhash expiration retries }, });
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 |
|---|---|
| On-chain agent: mint, owner, name, uri, memberNumber (), nonTransferable, additionalMetadata |
| ERC-8004 metadata with services, trust mechanisms |
| Simplified feedback input (FeedbackPublicV1) |
| Feedback with value, tags, message, createdAt, counterparty |
| Raw content: value, valueDecimals, tag1, tag2, m (message), endpoint, reviewer, feedbackURI, feedbackHash |
| Aggregated count + averageValue |
| Identity + registrationFile + optional feedbackStats |
| Enum: Positive (2), Negative (0), Neutral (1) |
| 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) { // Same taskRef + counterparty + agent already exists } }
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
with the CLI, or ensure the correct--keypair /path/to/original.json
KeyPairSigner in SDK.owner - 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
andrpcUrl
.photonRpcUrl