npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@exodus/safeguard-solana

v1.2.1

Published

Solana smart contract clients for Exodus Safeguard

Downloads

3,134

Readme

Safeguard Delegation Program

A Solana smart contract enabling multi-agent delegation for SPL tokens. Users authorize multiple AI agent wallets with independent spending limits while maintaining a single SPL token delegation.

Table of Contents

Problem

Solana's SPL Token program only allows one delegate per token account. Safeguard needs multiple agent connections per external wallet, each with independent spending limits.

Solution: Proxy Delegate Pattern

The program acts as an intermediary between the user's token account and multiple agent wallets.

┌──────────────────┐     SPL Approve      ┌─────────────────────┐
│  User's Token    │─────────────────────>│  Safeguard Program  │
│  Account (ATA)   │   (one-time setup)   │  Authority PDA      │
└──────────────────┘                      └─────────────────────┘
                                                   │
                                                   │ Internal routing
                                                   ▼
                              ┌────────────────────┬────────────────────┬
                              │                    │                    │
                              ▼                    ▼                    ▼
                    ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
                    │ Agent Connection │ │ Agent Connection │ │ Agent Connection │
                    │    Wallet #1     │ │    Wallet #2     │ │    Wallet #3     │
                    │ (max spend: 100) │ │ (max spend: 500) │ │(max spend: 1000) │
                    └──────────────────┘ └──────────────────┘ └──────────────────┘

Program IDs

| Network | Program ID | | ------- | ---------------------------------------------- | | Devnet | 2NFrZTi8A51yYe5GMXQap2up1HLsGuB95oAaNexw2sHM | | Mainnet | 2NFrZTi8A51yYe5GMXQap2up1HLsGuB95oAaNexw2sHM |

TypeScript Packages

This repository ships two packages:

| Package | Description | | ------------------------------------ | -------------------------------------------------------- | | @exodus/safeguard-solana | TypeScript client for building and querying transactions | | @exodus/safeguard-solana-interface | IDL and TypeScript types for the on-chain program |

Installation

pnpm add @exodus/safeguard-solana
# Optional: only needed if you need the IDL or raw types directly
pnpm add @exodus/safeguard-solana-interface

Usage

import { SafeguardDelegationClient } from "@exodus/safeguard-solana";
import { safeguardDelegation } from "@exodus/safeguard-solana-interface";
import type { SafeguardDelegation } from "@exodus/safeguard-solana-interface";

const client = new SafeguardDelegationClient("https://api.devnet.solana.com");

// Derive PDA addresses
const vaultPda = client.deriveVaultPda({
  owner: ownerPublicKey,
  userTokenAccount,
});
const delegationPda = client.deriveDelegationPda({
  vault: vaultPda,
  agentWallet: agentWalletPublicKey,
});

// Build a transaction (returns base64-encoded serialized tx for the user to sign)
// payer can differ from owner to sponsor transaction fees on the owner's behalf
// mint is required so the client can create the owner's ATA if it doesn't exist yet
const tx = await client.buildInitializeVaultTx({
  owner: ownerPublicKey,
  payer: payerPublicKey, // covers rent and tx fee; pass ownerPublicKey if owner pays
  mint: usdcMintPublicKey,
  userTokenAccount,
  agent: {
    agentWallet: agentWalletPublicKey,
    limits: { label: "My AI Agent", maxSpendAmount },
  },
});

API Reference

| Method | Returns | Description | | -------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------- | | deriveVaultPda(params) | PublicKey | Derive vault PDA address | | deriveDelegationPda(params) | PublicKey | Derive delegation PDA address | | buildInitializeVaultTx(params) | Promise<string> | Serialized tx: create owner ATA (if needed) + vault; optionally bundles first agent if agent provided | | buildAddAgentTx(params) | Promise<string> | Serialized tx: add agent to existing vault | | buildRevokeAgentTx(params) | Promise<string> | Serialized tx: deactivate agent (account kept) | | buildRestoreAgentTx(params) | Promise<string> | Serialized tx: reactivate a revoked agent | | buildRemoveAgentTx(params) | Promise<string> | Serialized tx: permanently close agent account; automatically closes vault if it's the last active agent | | buildUpdateLimitsTx(params) | Promise<string> | Serialized tx: modify agent spending limits | | buildEmergencyRevokeTx(params) | Promise<string> | Serialized tx: pause vault + zero out SPL delegation (delegate field preserved, amount set to 0) | | buildUnpauseVaultTx(params) | Promise<string> | Serialized tx: unpause vault + SPL approve for newDelegatedAmount (required param) | | buildExecuteTransferTx(params) | Promise<string> | Serialized tx: agent executes a token transfer | | buildCloseVaultTx(params) | Promise<string> | Serialized tx: remove all agent accounts, SPL revoke, then close vault (funder reclaims rent) | | getAgentBalance(params) | Promise<AgentBalance> | Query current spend capacity for an agent | | getVaults(owner) | Promise<VaultInfo[]> | Fetch all vaults owned by a wallet | | getTokenVault(params) | Promise<VaultInfo\|null> | Fetch vault for a specific token mint |

All methods (except getVaults) accept a named param object. See the exported Build*Params, Derive*Params, and Get*Params interfaces in @exodus/safeguard-solana for the exact shape of each.

AgentBalance

getAgentBalance returns the current spending state for an agent:

interface AgentBalance {
  maxSpendAmount: NumberUnit; // Configured lifetime cap
  totalSpent: NumberUnit; // Amount used so far
  tokenAccountBalance: NumberUnit; // Actual balance in token account
  expendableBalance: NumberUnit; // min(remaining allowance, token balance)
}

expendableBalance is the amount the agent can actually spend right now — capped by both the remaining delegation allowance and the real token account balance.

AgentLimits

Used by buildAddAgentTx to configure an agent's spending parameters:

interface AgentLimits {
  label: string; // Human-readable name (max 64 chars)
  maxSpendAmount: NumberUnit; // Lifetime spending cap
  expiresAt?: number; // Optional Solana slot number for expiry
}

VaultInfo

Returned by getVaults and getTokenVault:

interface VaultInfo {
  vaultPda: PublicKey; // On-chain vault PDA address
  userTokenAccount: PublicKey; // Token account delegated to this vault
  mint: PublicKey; // SPL token mint address
  isPaused: boolean; // Whether the vault is emergency-paused
  activeDelegationsCount: number; // Number of active agents
}

Account Structures

DelegationVault

Created per user token account. Tracks total delegation and manages agent connections.

| Field | Type | Description | | -------------------------- | ------ | -------------------------------------------------------- | | owner | Pubkey | User's wallet address | | funder | Pubkey | Account that paid vault rent (receives it back on close) | | user_token_account | Pubkey | Token account that delegated to this vault | | mint | Pubkey | SPL token mint address | | total_delegated_amount | u64 | Amount approved via SPL Token | | active_delegations_count | u32 | Number of currently active agents | | total_delegations_count | u32 | Total agents ever added (including revoked/removed) | | is_paused | bool | Emergency freeze flag | | bump | u8 | PDA bump seed | | version | u8 | Account schema version |

AgentDelegation

Created per agent per vault. Tracks lifetime spending limit and usage.

| Field | Type | Description | | ------------------ | ----------- | ----------------------------------------------------- | | vault | Pubkey | Parent vault reference | | funder | Pubkey | Account that paid delegation rent (reclaims on close) | | agent_wallet | Pubkey | Authorized signer (server wallet) | | label | String | Human-readable name (max 64 chars) | | max_spend_amount | u64 | Lifetime spending cap | | total_spent | u64 | Cumulative amount spent | | is_active | bool | Whether delegation is active | | expires_at | Option | Optional expiry slot number | | bump | u8 | PDA bump seed | | version | u8 | Account schema version |

PDA Derivation

Vault PDA      = hash("vault",      owner,            userTokenAccount)
Delegation PDA = hash("delegation", vault,            agentWallet)

Each delegation is cryptographically bound to a specific vault — an agent authorized for Vault A cannot spend from Vault B.

Data Model

On-Chain Entities (Solana - Trustless)

erDiagram
    Mint ||--o{ TokenAccount : "mints"
    TokenAccount ||--o| DelegationVault : "delegates to"
    DelegationVault ||--o{ AgentDelegation : "authorizes"
    AgentDelegation }o--|| ServerWallet : "signed by"

    Mint {
        pubkey address PK
        u8 decimals
        string symbol
    }

    TokenAccount {
        pubkey address PK
        pubkey owner
        pubkey mint FK
        u64 amount
        pubkey delegate
        u64 delegated_amount
    }

    DelegationVault {
        pubkey owner PK
        pubkey funder
        pubkey user_token_account FK
        pubkey mint FK
        u64 total_delegated_amount
        u32 active_delegations_count
        u32 total_delegations_count
        bool is_paused
        u8 bump
        u8 version
    }

    AgentDelegation {
        pubkey vault FK
        pubkey funder
        pubkey agent_wallet FK
        string label
        u64 max_spend_amount
        u64 total_spent
        bool is_active
        u64 expires_at
        u8 bump
        u8 version
    }

    ServerWallet {
        pubkey address PK
    }

Instructions

| Instruction | Signer | Description | | ------------------ | ------ | ----------------------------------------------------------------------------------------- | | initialize_vault | Owner | Create vault PDA; the TypeScript client optionally bundles add_agent in the same tx | | add_agent | Owner | Authorize an additional agent with a spending limit; re-approves SPL delegation | | revoke_agent | Owner | Deactivate agent (account kept, can be restored); re-approves SPL with reduced total | | restore_agent | Owner | Reactivate a previously revoked agent; re-approves SPL with restored total | | remove_agent | Owner | Permanently close agent account, return rent; re-approves SPL with remaining amount | | execute_transfer | Agent | Transfer within lifetime spend limit | | update_limits | Owner | Modify agent max_spend_amount and/or expiry; re-approves SPL delegation when amount grows | | emergency_revoke | Owner | Pause vault + zero out SPL delegation amount (delegate field preserved; amount set to 0) | | unpause_vault | Owner | Unpause vault and re-approve SPL delegation |

revoke_agent vs remove_agent

Both deactivate an agent, but differ in reversibility and account lifecycle:

| Aspect | revoke_agent | remove_agent | | --------------- | --------------------------------- | ------------------------------ | | Agent status | Deactivated (is_active = false) | Deleted | | Account | Kept on-chain | Closed, rent returned to owner | | Reversible? | Yes — via restore_agent | No | | SPL re-approval | Yes — reduces total allocated | Yes — reduces total allocated | | Use case | Temporary suspension | Permanent removal |

restore_agent will fail if the vault is paused, the delegation has expired, or the vault has reached the maximum agent count.

Sequence Diagrams

1. Vault Initialization

initialize_vault atomically creates the vault PDA, the first agent delegation, and calls the SPL Token approve CPI — all in a single transaction. The user signs only once.

sequenceDiagram
    participant User as User Wallet
    participant Frontend as Safeguard Frontend
    participant Backend as Auth Server
    participant Solana as Solana Network
    participant Program as Safeguard Program

    User->>Frontend: Connect wallet
    Frontend->>User: Request wallet signature
    User->>Frontend: Sign authentication

    Frontend->>Backend: Create vault + first agent request
    Backend->>Backend: Generate vault PDA + delegation PDA
    Backend->>Frontend: Return initialize_vault transaction

    Frontend->>User: Request user signature (initialize_vault tx)
    User->>Solana: Sign & send initialize_vault tx
    Program->>Program: Create DelegationVault account
    Program->>Program: Create AgentDelegation account
    Program->>Solana: CPI: SPL Token Approve (vault PDA as delegate)
    Program->>Solana: Emit VaultInitialized event
    Solana-->>Backend: Transaction confirmed

    Backend->>Frontend: Vault + agent created successfully
    Frontend->>User: Display vault status

2. MCP Tool Execution (Transfer)

sequenceDiagram
    participant Agent as AI Agent (ChatGPT/etc)
    participant MCP as MCP Server
    participant Backend as Auth Server
    participant Policy as Policy Evaluator
    participant Solana as Solana Network
    participant Program as Safeguard Program

    Agent->>MCP: tools/call transaction_send
    MCP->>Backend: POST /api/mcp (with API key)

    Backend->>Backend: Validate API key
    Backend->>Backend: Load agent connection

    Backend->>Policy: Evaluate transfer request
    Policy->>Policy: Check off-chain rules
    Policy-->>Backend: Allowed (or denied)

    alt Policy Denied
        Backend-->>MCP: Error: Policy violation
        MCP-->>Agent: Transfer denied by policy
    end

    Backend->>Solana: Call execute_transfer
    Program->>Program: Check vault not paused
    Program->>Program: Check delegation active
    Program->>Program: Check not expired
    Program->>Program: Check cumulative spend <= max_spend_amount

    alt Limit Exceeded
        Program-->>Backend: Error: SpendLimitExceeded
        Backend-->>MCP: Error: On-chain limit exceeded
        MCP-->>Agent: Transfer denied by limits
    end

    Program->>Solana: CPI: SPL Token Transfer
    Program->>Program: Update total_spent
    Program->>Solana: Emit TransferExecuted event
    Solana-->>Backend: Transaction confirmed

    Backend-->>MCP: Success response
    MCP-->>Agent: Transfer completed

3. Agent Revoke and Restore

revoke_agent deactivates an agent without closing its account — useful for temporary suspensions. restore_agent reactivates it.

sequenceDiagram
    participant User as User Wallet
    participant Frontend as Safeguard Frontend
    participant Backend as Auth Server
    participant Solana as Solana Network
    participant Program as Safeguard Program

    User->>Frontend: Pause agent connection
    Frontend->>Backend: POST /agent-connections/:id/revoke
    Backend->>Solana: Call revoke_agent

    Program->>Program: Set delegation.is_active = false
    Program->>Program: Decrement vault.active_delegations_count
    Program->>Program: Reduce vault.total_allocated by max_spend_amount
    Program->>Solana: CPI: SPL Token Approve (updated total)
    Program->>Solana: Emit AgentRevoked event
    Program->>Solana: Emit VaultStateChanged event
    Solana-->>Backend: Transaction confirmed

    Backend->>Backend: Update agent status in DB
    Backend-->>Frontend: Agent paused
    Frontend->>User: Display paused status

    Note over User,Program: Agent can no longer execute transfers

    User->>Frontend: Restore agent connection
    Frontend->>Backend: POST /agent-connections/:id/restore
    Backend->>Solana: Call restore_agent

    Program->>Program: Check vault not paused
    Program->>Program: Check max agents not reached
    Program->>Program: Check delegation not expired
    Program->>Program: Set delegation.is_active = true
    Program->>Program: Increment vault.active_delegations_count
    Program->>Program: Restore vault.total_allocated by max_spend_amount
    Program->>Solana: CPI: SPL Token Approve (updated total)
    Program->>Solana: Emit AgentRestored event
    Program->>Solana: Emit VaultStateChanged event
    Solana-->>Backend: Transaction confirmed

    Backend->>Backend: Update agent status in DB
    Backend-->>Frontend: Agent restored
    Frontend->>User: Display active status

4. Emergency Revoke

sequenceDiagram
    participant User as User Wallet
    participant Frontend as Safeguard Frontend
    participant Backend as Auth Server
    participant Solana as Solana Network
    participant Program as Safeguard Program

    User->>Frontend: Click Emergency Revoke
    Frontend->>User: Confirm action
    User->>Frontend: Confirm

    Frontend->>Backend: POST /vaults/:id/emergency-revoke
    Backend->>Solana: Call emergency_revoke

    Program->>Program: Set vault.is_paused = true
    Program->>Program: Set total_delegated_amount = 0
    Program->>Solana: CPI: SPL Token Approve(0)
    Program->>Solana: Emit VaultPaused event
    Program->>Solana: Emit EmergencyRevokeTriggered event
    Solana-->>Backend: Transaction confirmed

    Backend->>Backend: Mark all agents as revoked
    Backend-->>Frontend: Revoke successful
    Frontend->>User: Display revoked status

    Note over User,Program: All agent transfers now blocked

5. Agent Removal

When removing an agent the TypeScript client checks active_delegations_count on-chain. If the agent being removed is the last active one, the client automatically batches remove_agent + SPL revoke + close_vault into a single transaction, closing all remaining delegation PDAs (including any revoked ones) and the vault itself. The vault funder (not the owner) signs the close_vault instruction and reclaims the vault rent.

sequenceDiagram
    participant User as User Wallet
    participant Frontend as Safeguard Frontend
    participant Backend as Auth Server
    participant Client as TS Client
    participant Solana as Solana Network
    participant Program as Safeguard Program

    User->>Frontend: Remove agent connection
    Frontend->>User: Confirm removal
    User->>Frontend: Confirm

    Frontend->>Backend: DELETE /agent-connections/:id
    Backend->>Client: buildRemoveAgentTx(params)
    Client->>Solana: Fetch vault (active_delegations_count)

    alt Last active agent (count == 1)
        Client->>Client: Build close_vault tx (removes all PDAs + SPL revoke + vault)
        Backend->>Solana: Submit close_vault tx (signed by funder)
        Program->>Program: Close all delegation accounts
        Client->>Solana: SPL Token Revoke (clears delegate on token account)
        Program->>Program: Close vault account (checks delegate cleared)
        Program->>Program: Return vault rent to funder
        Program->>Solana: Emit AgentRemoved + VaultClosed events
        Solana-->>Backend: Transaction confirmed
        Backend->>Backend: Delete vault + delegation records from DB
    else More agents remain
        Client->>Client: Build remove_agent tx
        Backend->>Solana: Submit remove_agent tx (signed by user)
        Program->>Program: Decrement vault.active_delegations_count
        Program->>Program: Reduce vault.total_allocated
        Program->>Program: Close delegation account
        Program->>Program: Return rent to owner
        Program->>Solana: CPI: SPL Token Approve (updated total)
        Program->>Solana: Emit AgentRemoved event
        Solana-->>Backend: Transaction confirmed
        Backend->>Backend: Remove agent delegation record from DB
    end

    Backend-->>Frontend: Agent removed
    Frontend->>User: Display updated agent list

Security & Trust Model

User Controls

  • Create vault with first agent (single transaction)
  • Add/remove/revoke/restore additional agents
  • Update spending limits
  • Emergency freeze all
  • Revoke SPL delegation directly (bypass program)

Agent Controls (within limits)

  • Execute transfers up to max_spend_amount lifetime cap
  • Cannot exceed the lifetime spend limit
  • Cannot transfer after expiry
  • Cannot reactivate revoked delegation

Authorization Requirements

Before any agent can spend from a user's wallet, a user-signed transaction is required:

| initialize_vault (first agent) | add_agent (additional) | | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | User signs one transaction that atomically creates the vault, first AgentDelegation, and approves SPL token delegation | User (vault owner) signs to authorize each new agent_wallet pubkey with its spending limit |

Without a user signature, nothing can be created on-chain.

The Safeguard backend cannot unilaterally add itself as an agent — only the vault owner's signature can authorize an agent.

PDA Isolation (Cross-Vault Protection)

Each agent delegation is cryptographically bound to a specific vault:

Delegation PDA = hash("delegation", vault_pda, agent_wallet)
                                    ─────────
                                        │
                                        └── Agent authorized for Vault A
                                            CANNOT spend from Vault B

What is Trustless (On-Chain Guarantees)

These guarantees are enforced by the Solana program and cannot be bypassed by anyone, including Safeguard:

| Guarantee | Enforcement | | ------------------------------------ | ----------------------------------------------- | | Lifetime spend limit cannot exceed | On-chain cumulative check before every transfer | | Only authorized agents can transfer | PDA derivation + signature verification | | Users can always emergency revoke | Owner signature required, always succeeds | | Agents cannot reactivate themselves | Only owner can call add_agent/restore_agent | | Limits cannot be increased by agents | Only owner can call update_limits | | Expired delegations cannot transfer | On-chain timestamp check |

What Requires Trusting Safeguard (Off-Chain)

| Aspect | Trust Requirement | | -------------------------- | --------------------------------------- | | Server wallet private keys | Safeguard stores these securely | | API key → wallet mapping | Database integrity | | Policy enforcement | Off-chain rules applied before on-chain | | Transfer initiation | Only on legitimate AI agent requests |

Worst Case: Backend Compromise

If an attacker fully compromises the Safeguard backend:

| Attacker CAN | Attacker CANNOT | | --------------------------------------------------- | -------------------------------------------- | | Access server wallet keys | Exceed on-chain limits | | Initiate transfers for any agent they have keys for | Spend from vaults that never added the agent | | Bypass off-chain policy rules | Prevent user emergency revoke | | | Modify on-chain limits | | | Reactivate revoked agents |

Maximum damage = sum of (max_spend_amount - total_spent) across all active agents.

The on-chain limits act as a "blast radius cap" — even total backend compromise cannot exceed the spending limits users configured.

User Self-Protection

| Action | Effect | | ----------------------- | ----------------------------- | | Set conservative limits | Reduces maximum possible loss | | Set expiry dates | Automatic agent deactivation | | Monitor on-chain events | Detect unauthorized transfers | | Emergency revoke | Instantly stops all agents | | Direct SPL revoke | Bypass Safeguard entirely |

Trust Model Summary

| Layer | Trust Model | Failure Impact | | ------------------ | --------------- | -------------------------- | | On-chain program | Trustless | Cannot fail (code is law) | | On-chain limits | Trustless | Cannot be exceeded | | User authorization | Trustless | Cannot be bypassed | | Backend security | Trust Safeguard | Limited by on-chain limits | | Policy enforcement | Trust Safeguard | Limited by on-chain limits |

Design Philosophy: The on-chain program assumes the backend might be compromised and enforces hard limits regardless. Users should set max_spend_amount to the maximum they're comfortable losing in a worst-case scenario.

Integration with Safeguard

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────────────┐
│                              SAFEGUARD SYSTEM                                   │
│                                                                                 │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐       │
│  │   Frontend  │───>│ Auth Server │───>│   Policy    │───>│  On-Chain   │       │
│  │   (Next.js) │    │  (Fastify)  │    │  Evaluator  │    │  (Solana)   │       │
│  └─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘       │
│        │                  │                  │                  │               │
│        │                  │                  │                  │               │
│        ▼                  ▼                  ▼                  ▼               │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐       │
│  │    User     │    │   Agent     │    │   Wallet    │    │ Safeguard   │       │
│  │   Wallet    │    │ Connection  │    │   Policy    │    │ Delegation  │       │
│  │  (Browser)  │    │   (DB)      │    │   Rules     │    │  Program    │       │
│  └─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘       │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘

Limit Enforcement Model

The system uses a complementary limit model:

  • Off-chain (Policy Evaluator): Nuanced rules (time-of-day, recipient whitelist, AI-based risk)
  • On-chain (Solana Program): Hard safety net (lifetime spend cap via max_spend_amount)

This provides defense-in-depth: even if the off-chain system is compromised, the on-chain limit prevents catastrophic losses.

API Key vs On-Chain Relationship

┌─────────────────────────────────────────────────────────────────────────┐
│                                                                         │
│                         OFF-CHAIN (Backend)                             │
│  ┌─────────────┐      ┌─────────────────────┐      ┌────────────────┐  │
│  │   API Key   │─────>│   Agent Connection  │─────>│ Server Wallet  │  │
│  │  (secret)   │      │      (database)     │      │   (keypair)    │  │
│  └─────────────┘      └─────────────────────┘      └────────────────┘  │
│                                                            │           │
│        Used for MCP            Stores mapping              │           │
│        authentication          and metadata                │           │
│                                                            │           │
└────────────────────────────────────────────────────────────┼───────────┘
                                                             │
                                          Signs transactions │
                                          with private key   │
                                                             ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                                                                         │
│                          ON-CHAIN (Solana)                              │
│                                                                         │
│  ┌─────────────────────┐           ┌─────────────────────┐             │
│  │   DelegationVault   │           │   AgentDelegation   │             │
│  │   (user's vault)    │◄─────────►│   agent_wallet =    │             │
│  │                     │           │   server wallet     │             │
│  └─────────────────────┘           │   PUBKEY only       │             │
│                                    └─────────────────────┘             │
│                                                                         │
│        The API key NEVER touches the blockchain.                        │
│        Only the server wallet's PUBLIC KEY is stored on-chain.          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Development

Prerequisites

  • Rust 1.89+ (for edition 2024 support)
  • Solana CLI 3.0+
  • Anchor CLI 0.32+
  • Node.js 18+

Build

From the repo root:

pnpm contracts:solana:build

Or from the package directory:

# Build TypeScript client + copy IDL to dist/
pnpm build

# Rebuild IDL only (after Rust program changes)
pnpm idl:build

Test

From the repo root:

pnpm contracts:solana:test

Or from the package directory:

pnpm test

anchor test handles starting the local validator, deploying the program, and running the full test suite automatically.

Deploy

1. Set up your deploy wallet

Copy the env example and set the absolute path to your deploy keypair:

cp .env.example .env

Edit .env:

DEPLOY_WALLET_PATH=/absolute/path/to/deploy-wallet.json
  • DEPLOY_WALLET_PATH — the upgrade authority wallet used to sign and fund the upgrade transaction. This wallet must be the current upgrade authority for program 2NFrZTi8A51yYe5GMXQap2up1HLsGuB95oAaNexw2sHM.

Note: use an absolute path — ~ is not expanded in .env files.

2. Fund the deploy wallet

The deploy wallet needs enough SOL to cover the program data account rent (~2.4 SOL for the current binary size). Use the Solana faucet UI to airdrop devnet SOL:

https://faucet.solana.com/

Paste the deploy wallet address and request SOL. You can check the balance with:

solana balance <DEPLOY_WALLET_ADDRESS> --url devnet

3. Transfer upgrade authority (first time only)

If upgrading an existing program, transfer the upgrade authority to your deploy wallet:

solana program set-upgrade-authority <PROGRAM_ID> \
  --new-upgrade-authority <DEPLOY_WALLET_ADDRESS> \
  --skip-new-upgrade-authority-signer-check \
  --url devnet

4. Deploy

From the repo root:

# Devnet
pnpm contracts:solana:deploy:devnet

# Mainnet
pnpm contracts:solana:deploy:mainnet

Reference

Error Codes

| Code | Name | Description | | ---- | ----------------------------- | -------------------------------------------------------------- | | 6000 | VaultPaused | Vault is paused - no operations allowed | | 6001 | DelegationInactive | Delegation is not active | | 6002 | DelegationExpired | Delegation has expired | | 6003 | SpendLimitExceeded | Transfer exceeds lifetime spend limit | | 6004 | InvalidTokenAccountOwner | Token account not owned by signer | | 6005 | MintMismatch | Token mint doesn't match vault | | 6006 | InsufficientDelegation | SPL delegation insufficient | | 6007 | ArithmeticOverflow | Math overflow | | 6008 | LabelTooLong | Agent label exceeds 64 chars | | 6009 | Unauthorized | Only owner can perform action | | 6010 | InvalidExpiry | Expiry must be in the future | | 6011 | MaxAgentsReached | Vault has reached the maximum agent count | | 6012 | DelegationAlreadyActive | Delegation is already active | | 6013 | InvalidAgentWallet | Agent wallet must differ from vault owner | | 6014 | InvalidAmount | Amount must be greater than zero | | 6015 | AgentVaultMismatch | Agent account does not belong to this vault | | 6016 | VaultStillDelegated | Token account still delegating to vault; revoke before closing | | 6017 | VaultNotPaused | Vault is not paused | | 6018 | InvalidDelegationAccountCount | Remaining accounts count must match total delegation count |