npm stats
  • Search
  • About
  • Repo
  • Sponsor
  • more
    • Search
    • About
    • Repo
    • Sponsor

Made by Antonio Ramirez

@exodus/safeguard-solana

1.2.1

@marcos.kichel

npmHomeRepoSnykSocket
Downloads:39
$ npm install @exodus/safeguard-solana
DailyWeeklyMonthlyYearly

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
  • Solution: Proxy Delegate Pattern
  • Program IDs
  • TypeScript Client
    • Installation
    • Usage
    • API Reference
    • AgentBalance
  • Account Structures
    • DelegationVault
    • AgentDelegation
    • PDA Derivation
  • Data Model
  • Instructions
    • revoke_agent vs remove_agent
  • Sequence Diagrams
    • 1. Vault Initialization
    • 2. MCP Tool Execution (Transfer)
    • 3. Agent Revoke and Restore
    • 4. Emergency Revoke
    • 5. Agent Removal
  • Security & Trust Model
    • Authorization Requirements
    • PDA Isolation
    • What is Trustless
    • What Requires Trusting Safeguard
    • Worst Case: Backend Compromise
    • User Self-Protection
  • Integration with Safeguard
  • Development
    • Build
    • Test
    • Deploy
  • Reference
    • Error Codes

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

NetworkProgram ID
Devnet2NFrZTi8A51yYe5GMXQap2up1HLsGuB95oAaNexw2sHM
Mainnet2NFrZTi8A51yYe5GMXQap2up1HLsGuB95oAaNexw2sHM

TypeScript Packages

This repository ships two packages:

PackageDescription
@exodus/safeguard-solanaTypeScript client for building and querying transactions
@exodus/safeguard-solana-interfaceIDL 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

MethodReturnsDescription
deriveVaultPda(params)PublicKeyDerive vault PDA address
deriveDelegationPda(params)PublicKeyDerive 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.

FieldTypeDescription
ownerPubkeyUser's wallet address
funderPubkeyAccount that paid vault rent (receives it back on close)
user_token_accountPubkeyToken account that delegated to this vault
mintPubkeySPL token mint address
total_delegated_amountu64Amount approved via SPL Token
active_delegations_countu32Number of currently active agents
total_delegations_countu32Total agents ever added (including revoked/removed)
is_pausedboolEmergency freeze flag
bumpu8PDA bump seed
versionu8Account schema version

AgentDelegation

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

FieldTypeDescription
vaultPubkeyParent vault reference
funderPubkeyAccount that paid delegation rent (reclaims on close)
agent_walletPubkeyAuthorized signer (server wallet)
labelStringHuman-readable name (max 64 chars)
max_spend_amountu64Lifetime spending cap
total_spentu64Cumulative amount spent
is_activeboolWhether delegation is active
expires_atOptionOptional expiry slot number
bumpu8PDA bump seed
versionu8Account 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

InstructionSignerDescription
initialize_vaultOwnerCreate vault PDA; the TypeScript client optionally bundles add_agent in the same tx
add_agentOwnerAuthorize an additional agent with a spending limit; re-approves SPL delegation
revoke_agentOwnerDeactivate agent (account kept, can be restored); re-approves SPL with reduced total
restore_agentOwnerReactivate a previously revoked agent; re-approves SPL with restored total
remove_agentOwnerPermanently close agent account, return rent; re-approves SPL with remaining amount
execute_transferAgentTransfer within lifetime spend limit
update_limitsOwnerModify agent max_spend_amount and/or expiry; re-approves SPL delegation when amount grows
emergency_revokeOwnerPause vault + zero out SPL delegation amount (delegate field preserved; amount set to 0)
unpause_vaultOwnerUnpause vault and re-approve SPL delegation

revoke_agent vs remove_agent

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

Aspectrevoke_agentremove_agent
Agent statusDeactivated (is_active = false)Deleted
AccountKept on-chainClosed, rent returned to owner
Reversible?Yes — via restore_agentNo
SPL re-approvalYes — reduces total allocatedYes — reduces total allocated
Use caseTemporary suspensionPermanent 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 delegationUser (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:

GuaranteeEnforcement
Lifetime spend limit cannot exceedOn-chain cumulative check before every transfer
Only authorized agents can transferPDA derivation + signature verification
Users can always emergency revokeOwner signature required, always succeeds
Agents cannot reactivate themselvesOnly owner can call add_agent/restore_agent
Limits cannot be increased by agentsOnly owner can call update_limits
Expired delegations cannot transferOn-chain timestamp check

What Requires Trusting Safeguard (Off-Chain)

AspectTrust Requirement
Server wallet private keysSafeguard stores these securely
API key → wallet mappingDatabase integrity
Policy enforcementOff-chain rules applied before on-chain
Transfer initiationOnly on legitimate AI agent requests

Worst Case: Backend Compromise

If an attacker fully compromises the Safeguard backend:

Attacker CANAttacker CANNOT
Access server wallet keysExceed on-chain limits
Initiate transfers for any agent they have keys forSpend from vaults that never added the agent
Bypass off-chain policy rulesPrevent 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

ActionEffect
Set conservative limitsReduces maximum possible loss
Set expiry datesAutomatic agent deactivation
Monitor on-chain eventsDetect unauthorized transfers
Emergency revokeInstantly stops all agents
Direct SPL revokeBypass Safeguard entirely

Trust Model Summary

LayerTrust ModelFailure Impact
On-chain programTrustlessCannot fail (code is law)
On-chain limitsTrustlessCannot be exceeded
User authorizationTrustlessCannot be bypassed
Backend securityTrust SafeguardLimited by on-chain limits
Policy enforcementTrust SafeguardLimited 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

CodeNameDescription
6000VaultPausedVault is paused - no operations allowed
6001DelegationInactiveDelegation is not active
6002DelegationExpiredDelegation has expired
6003SpendLimitExceededTransfer exceeds lifetime spend limit
6004InvalidTokenAccountOwnerToken account not owned by signer
6005MintMismatchToken mint doesn't match vault
6006InsufficientDelegationSPL delegation insufficient
6007ArithmeticOverflowMath overflow
6008LabelTooLongAgent label exceeds 64 chars
6009UnauthorizedOnly owner can perform action
6010InvalidExpiryExpiry must be in the future
6011MaxAgentsReachedVault has reached the maximum agent count
6012DelegationAlreadyActiveDelegation is already active
6013InvalidAgentWalletAgent wallet must differ from vault owner
6014InvalidAmountAmount must be greater than zero
6015AgentVaultMismatchAgent account does not belong to this vault
6016VaultStillDelegatedToken account still delegating to vault; revoke before closing
6017VaultNotPausedVault is not paused
6018InvalidDelegationAccountCountRemaining accounts count must match total delegation count