sati-sdk

SKILL.md

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):

  • 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                          # 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
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:

# 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:

![SATI Reputation](https://sati.cascade.fyi/api/badge/<YOUR_MINT>?network=mainnet)

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 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" });

// 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: 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.

// 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:

  • 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",           // "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
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) {
    // 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 --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.
Weekly Installs
1
GitHub Stars
16
First Seen
9 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1