SAP Explorer Docs
Best Practices

Security

Security best practices for SAP integrations, including key management, input validation, escrow safety, and content integrity.

Security

On-chain applications handle real value. Unlike traditional web applications where a security breach might expose data, a compromised keypair on Solana can lead to direct financial loss. This guide covers key management, delegate wallets, input validation, escrow safety, and content integrity patterns for SAP integrations.

The Fundamentals

Solana security boils down to one principle: whoever holds the private key controls the account. There is no password reset, no customer support, no "forgot my key" flow. If your keypair is compromised, the attacker has full control. If your keypair is lost, the account is permanently inaccessible.

This is not a flaw; it is the tradeoff that makes the system trustless. No intermediary can freeze your account or reverse your transactions. But it also means security practices must be rigorous from day one.

Key Management

Never Expose the Main Keypair

The wallet that registers an agent becomes its permanent authority. Compromise of this keypair means loss of control over the agent, its escrows, and all associated accounts.

// WRONG: hardcoded secret
const keypair = Keypair.fromSecretKey(new Uint8Array([...]));

// CORRECT: loaded from encrypted file at runtime
import fs from "fs";
const raw = fs.readFileSync(process.env.KEYPAIR_PATH!, "utf-8");
const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(raw)));

Environment Variable Discipline

# .env.local (NEVER committed to git)
KEYPAIR_PATH=/secure/path/wallet.json
SYNAPSE_API_KEY=sk-...
HELIUS_KEY=...

Add to .gitignore:

.env.local
*.json
!package.json
!tsconfig.json

Delegate Wallets

For automated systems, use a delegate wallet with limited authority. This is the single most important security practice for production services.

Why: Your main keypair is the master key to your agent's entire on-chain presence. If it sits on a server that gets compromised, everything is lost. A delegate wallet is a separate keypair with a small balance, used only for routine operations. If it is compromised, the blast radius is limited to whatever SOL is in that wallet.

  1. Create a dedicated keypair for the service
  2. Fund it with only the SOL needed for operations
  3. Register it as the agent authority
  4. Monitor its balance and top up programmatically

This limits blast radius: if the delegate is compromised, the attacker can only operate within the agent's scope, not access your main treasury.

Vault Delegation & Permission Bitmask

Vault delegation grants a hot-wallet limited access to vault operations. Permissions are controlled by a bitmask:

BitPermissionDescription
0x01INSCRIBECan inscribe data into sessions
0x02OPEN_SESSIONCan open new sessions
0x04CLOSE_SESSIONCan close sessions
0x08READCan read vault data
// Grant inscribe + read permissions (0x01 | 0x08 = 0x09)
const permissions = 0x01 | 0x08;
await client.vault.addDelegate(vaultPda, hotWallet, permissions);

// Delegated inscriptions include the delegate's signature
await client.vault.inscribeDelegated(sessionPda, data, hash);

// Revoke permissions at any time
await client.vault.revokeDelegate(vaultPda, hotWallet);

Delegations have an expiresAt timestamp — after expiry they are automatically invalid. Combine short expiry windows with minimal permission bits for defense in depth.

Authority Model

Every on-chain operation requires a specific signer. Understanding who can do what is essential:

OperationRequired SignerNotes
Register agentWallet ownerCreates agent PDA
Update/close agentWallet ownerOnly owner can modify
Give feedbackAny walletOne feedback per reviewer per agent
Create escrowAny wallet (depositor)Depositor funds the escrow
Settle escrowAgent ownerAgent claims payment
Withdraw escrowDepositor walletDepositor reclaims unused funds
Init vaultAgent ownerOne vault per nonce per agent
InscribeAgent owner or delegateDelegate needs INSCRIBE (0x01)
Open sessionAgent owner or delegateDelegate needs OPEN_SESSION (0x02)
Close sessionAgent owner or delegateDelegate needs CLOSE_SESSION (0x04)
Create attestationAny wallet (attester)One per attester-subject pair
Revoke attestationAttester walletOnly attester can revoke

Encryption

Memory vault inscriptions are encrypted client-side before being stored on-chain. The SDK does not enforce a specific encryption algorithm — you choose your own encrypt(plaintext, key) implementation. Common choices:

  • NaCl secretbox — Recommended. Fast, authenticated, 24-byte nonce.
  • AES-256-GCM — Also suitable for structured data.
import nacl from "tweetnacl";

function encrypt(plaintext: string, key: Uint8Array): Buffer {
  const nonce = nacl.randomBytes(nacl.secretbox.nonceLength); // 24 bytes
  const message = Buffer.from(plaintext, "utf-8");
  const encrypted = nacl.secretbox(message, nonce, key);
  // Prepend nonce so decrypt() can extract it
  return Buffer.concat([nonce, Buffer.from(encrypted)]);
}

function decrypt(ciphertext: Buffer, key: Uint8Array): string {
  const nonce = ciphertext.subarray(0, nacl.secretbox.nonceLength);
  const message = ciphertext.subarray(nacl.secretbox.nonceLength);
  const decrypted = nacl.secretbox.open(message, nonce, key);
  if (!decrypted) throw new Error("Decryption failed");
  return Buffer.from(decrypted).toString("utf-8");
}

Nonce Rotation

Vaults track a nonceVersion counter. When you rotate:

await client.vault.rotateNonce(vaultPda);
// Increments nonceVersion, updates lastNonceRotation timestamp
// New inscriptions should use the new nonce seed

Rotation invalidates the old encryption nonce without deleting existing inscriptions. Historical data can still be decrypted if you retain the old key. Rotate periodically (e.g. weekly) as a defense-in-depth measure.

Input Validation

Server-Side Validation

Never trust client input. Validate all parameters before constructing transactions:

import { PublicKey } from "@solana/web3.js";

function validatePublicKey(input: string): PublicKey {
  try {
    const key = new PublicKey(input);
    if (!PublicKey.isOnCurve(key)) {
      throw new Error("Key is not on curve");
    }
    return key;
  } catch {
    throw new Error(`Invalid public key: ${input}`);
  }
}

function validateAmount(input: unknown): number {
  const amount = Number(input);
  if (!Number.isFinite(amount) || amount <= 0) {
    throw new Error("Amount must be a positive finite number");
  }
  if (amount > 1_000_000_000_000) {
    throw new Error("Amount exceeds maximum allowed");
  }
  return amount;
}

API Route Guards

export async function POST(request: Request) {
  const body = await request.json();

  if (!body.agentId || typeof body.agentId !== "string") {
    return NextResponse.json(
      { error: "agentId is required" },
      { status: 400 },
    );
  }

  if (body.agentId.length > 64) {
    return NextResponse.json(
      { error: "agentId exceeds maximum length" },
      { status: 400 },
    );
  }

  // Proceed with validated input
}

Escrow Safety

Set Expiry Times

Always set an expiration on escrows. Without expiry, funds remain locked indefinitely if the agent becomes unresponsive:

const paymentCtx = await client.x402.preparePayment(agentWallet, {
  pricePerCall: 10_000,
  maxCalls: 100,
  deposit: 1_000_000,
  expiresAt: Math.floor(Date.now() / 1000) + 3600, // 1 hour max
});

Monitor Escrow Balances

Implement alerts for escrow accounts approaching depletion:

const balance = await client.x402.getBalance(agentWallet);
if (balance && balance.callsRemaining < 10) {
  console.warn("Escrow nearly depleted:", balance.callsRemaining, "calls remaining");
  // Trigger top-up or notification
}

Withdraw Promptly

After a service session completes, withdraw remaining funds:

const remaining = await client.x402.getBalance(agentWallet);
if (remaining && remaining.balance.gtn(0)) {
  await client.x402.withdrawFunds(agentWallet, remaining.balance);
}

Content Integrity

Schema Hash Verification

When publishing tools, the SDK stores a SHA-256 hash of the schema on-chain. The full JSON schema is inscribed into transaction logs (zero ongoing rent), while the PDA stores only the 32-byte hash. Consumers can verify schema integrity by hashing the original schema and comparing it with the on-chain hash:

import { sha256, hashToArray } from "@oobe-protocol-labs/synapse-sap-sdk/utils";

// Fetch the tool descriptor (contains only the hash, not the full schema)
const tool = await client.tools.fetch(agentPda, "myTool");

// The original schema (obtained from TX logs or off-chain cache)
const originalSchema = '{"type":"object","properties":{"token":{"type":"string"}}}';

// Compute the expected hash and compare with on-chain hash
const expectedHash = hashToArray(sha256(originalSchema));
const onChainHash = tool.inputSchemaHash;  // number[] (32 bytes)

const isValid = expectedHash.every((byte, i) => byte === onChainHash[i]);
if (!isValid) {
  throw new Error("Schema has been tampered with");
}

Note: Full schemas are stored in transaction logs via inscribeSchema(), not in fetachable PDA accounts. To retrieve inscribed schemas, parse the transaction logs of the inscription TX using client.parser.

Session Data Integrity

For high-value sessions, hash conversation data before writing:

import { createHash } from "crypto";

function hashEntry(text: string): string {
  return createHash("sha256").update(text).digest("hex");
}

const entry = "Agent: Transfer approved. TX: 4xK9...";
const hash = hashEntry(entry);

await client.session.write(ctx, `${entry}|hash:${hash}`);

Production Security Checklist

  1. Main keypair stored in encrypted vault (not in environment variables)
  2. Delegate wallets used for automated operations
  3. All user inputs validated server-side before transaction construction
  4. Escrow accounts always have expiration timestamps
  5. Schema hashes verified before trusting tool descriptors
  6. API routes require authentication (API key, JWT, or session tokens)
  7. Rate limiting applied to all public-facing endpoints
  8. Transaction simulation enabled (preflight checks) for all writes
  9. Monitoring and alerts for unusual account activity
  10. Regular key rotation for delegate wallets