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.

install
source · Clone the upstream repo
git clone https://github.com/cascade-protocol/sati
Claude Code · Install into ~/.claude/skills/
git clone --depth=1 https://github.com/cascade-protocol/sati ~/.claude/skills/cascade-protocol-sati-sati-sdk
manifest: SKILL.md
safety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
  • makes HTTP requests (curl)
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content

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:

tag1value rangemeaning
starred
0-100Overall rating
reachable
0 or 1Health check (1 = reachable)
uptime
0-100Uptime percentage
responseTime
msLatency in milliseconds
successRate
0-100Success 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

TypeDescription
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

OperationCost
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)
DevnetFree (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
    .