This guide provides comprehensive documentation of the Solana plugin’s architecture, implementation, and advanced features.

Architecture Overview

The Solana plugin follows a modular architecture optimized for high-performance blockchain interactions:

┌─────────────────┐     ┌──────────────┐     ┌──────────────┐
│     Actions     │────▶│ SolanaService│────▶│  Solana RPC  │
│  (User Intent)  │     │  (Core Logic)│     │  Connection  │
└─────────────────┘     └──────────────┘     └──────────────┘
         │                      │                     │
         ▼                      ▼                     ▼
┌─────────────────┐     ┌──────────────┐     ┌──────────────┐
│   AI Templates  │     │   Providers  │     │ Birdeye API  │
│  (NLP Parsing)  │     │ (Wallet Data)│     │ (Price Data) │
└─────────────────┘     └──────────────┘     └──────────────┘

Core Components

SolanaService

The central service managing all Solana blockchain interactions:

export class SolanaService extends Service {
  static serviceType = 'solana-service';
  
  private connection: Connection;
  private keypair?: Keypair;
  private wallet?: Wallet;
  private cache: Map<string, { data: any; timestamp: number }> = new Map();
  private subscriptions: number[] = [];

  async initialize(runtime: IAgentRuntime): Promise<void> {
    // Initialize connection
    const rpcUrl = runtime.getSetting('SOLANA_RPC_URL') || 'https://api.mainnet-beta.solana.com';
    this.connection = new Connection(rpcUrl, {
      commitment: 'confirmed',
      wsEndpoint: rpcUrl.replace('https', 'wss')
    });

    // Initialize wallet
    const privateKey = runtime.getSetting('SOLANA_PRIVATE_KEY');
    if (privateKey) {
      this.keypair = await loadKeypair(privateKey);
      this.wallet = new Wallet(this.keypair);
    }

    // Start portfolio monitoring
    this.startPortfolioTracking();
    
    // Register with trader service if available
    this.registerWithTraderService(runtime);
  }

  private async startPortfolioTracking(): Promise<void> {
    // Initial fetch
    await this.fetchPortfolioData();
    
    // Set up periodic refresh (2 minutes)
    setInterval(() => this.fetchPortfolioData(), 120000);
    
    // Set up WebSocket subscriptions
    if (this.keypair) {
      this.setupAccountSubscriptions();
    }
  }
}

Actions

Transfer Action

Handles SOL and SPL token transfers with intelligent parsing:

export const transferAction: Action = {
  name: 'TRANSFER_SOLANA',
  description: 'Transfer SOL or SPL tokens on Solana',
  
  validate: async (runtime: IAgentRuntime) => {
    const privateKey = runtime.getSetting('SOLANA_PRIVATE_KEY');
    return !!privateKey;
  },

  handler: async (runtime, message, state, options, callback) => {
    try {
      // Extract parameters using AI
      const params = await extractTransferParams(runtime, message, state);
      
      // Get service instance
      const service = runtime.getService<SolanaService>('solana-service');
      
      // Execute transfer
      const result = await executeTransfer(service, params);
      
      callback?.({
        text: `Successfully transferred ${params.amount} ${params.token} to ${params.recipient}`,
        content: {
          success: true,
          signature: result.signature,
          amount: params.amount,
          token: params.token,
          recipient: params.recipient
        }
      });
    } catch (error) {
      callback?.({
        text: `Transfer failed: ${error.message}`,
        content: { error: error.message }
      });
    }
  },

  examples: [
    [
      {
        name: 'user',
        content: { text: 'Send 1 SOL to 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU' }
      },
      {
        name: 'assistant',
        content: { text: "I'll send 1 SOL to that address right away." }
      }
    ]
  ],

  similes: ['SEND_SOL', 'SEND_TOKEN_SOLANA', 'TRANSFER_SOL', 'PAY_SOL']
};

Swap Action

Token swapping using Jupiter aggregator:

export const swapAction: Action = {
  name: 'SWAP_SOLANA',
  description: 'Swap tokens on Solana using Jupiter',
  
  handler: async (runtime, message, state, options, callback) => {
    // Extract swap parameters
    const params = await extractSwapParams(runtime, message, state);
    
    // Get Jupiter quote
    const quote = await getJupiterQuote({
      inputMint: params.fromToken,
      outputMint: params.toToken,
      amount: params.amount,
      slippageBps: params.slippage * 100 // Convert to basis points
    });
    
    // Execute swap
    const result = await executeJupiterSwap(
      service.connection,
      service.wallet,
      quote
    );
    
    callback?.({
      text: `Swapped ${params.fromAmount} ${params.fromSymbol} for ${formatAmount(quote.outAmount)} ${params.toSymbol}`,
      content: {
        success: true,
        signature: result.signature,
        fromAmount: params.fromAmount,
        toAmount: formatAmount(quote.outAmount),
        route: quote.routePlan
      }
    });
  }
};

Providers

Wallet Provider

Supplies comprehensive wallet and portfolio data:

export const walletProvider: Provider = {
  name: 'solana-wallet',
  description: 'Provides Solana wallet information and portfolio data',
  
  get: async (runtime: IAgentRuntime, message?: Memory, state?: State) => {
    const service = runtime.getService<SolanaService>('solana-service');
    const portfolioData = await service.getCachedPortfolioData();
    
    if (!portfolioData) {
      return 'Wallet data unavailable';
    }
    
    // Format portfolio for AI context
    const summary = formatPortfolioSummary(portfolioData);
    const tokenList = formatTokenBalances(portfolioData.tokens);
    
    return `Solana Wallet Portfolio:
Total Value: $${portfolioData.totalUsd.toFixed(2)} (${portfolioData.totalSol.toFixed(4)} SOL)

Token Balances:
${tokenList}

SOL Price: $${portfolioData.solPrice.toFixed(2)}
Last Updated: ${new Date(portfolioData.lastUpdated).toLocaleString()}`;
  }
};

Templates

AI prompt templates for natural language understanding:

export const transferTemplate = `Given the recent messages:
{{recentMessages}}

And wallet information:
{{walletInfo}}

Extract the following for a Solana transfer:
- Amount to send (number only)
- Token to send (SOL or token symbol/address)
- Recipient address or domain

Respond with:
<response>
  <amount>string</amount>
  <token>string</token>
  <recipient>string</recipient>
</response>`;

export const swapTemplate = `Given the swap request:
{{recentMessages}}

And available tokens:
{{walletInfo}}

Extract swap details:
- Input token (symbol or address)
- Input amount (or "all" for max)
- Output token (symbol or address)
- Slippage tolerance (percentage, default 1%)

<response>
  <inputToken>string</inputToken>
  <inputAmount>string</inputAmount>
  <outputToken>string</outputToken>
  <slippage>number</slippage>
</response>`;

Advanced Features

Keypair Management

The plugin supports multiple key formats and secure handling:

export async function loadKeypair(privateKey: string): Promise<Keypair> {
  try {
    // Try base58 format first
    const decoded = bs58.decode(privateKey);
    if (decoded.length === 64) {
      return Keypair.fromSecretKey(decoded);
    }
  } catch (e) {
    // Not base58, try base64
  }

  try {
    // Try base64 format
    const decoded = Buffer.from(privateKey, 'base64');
    if (decoded.length === 64) {
      return Keypair.fromSecretKey(decoded);
    }
  } catch (e) {
    // Not base64
  }

  // Try JSON format (Solana CLI)
  try {
    const parsed = JSON.parse(privateKey);
    if (Array.isArray(parsed)) {
      return Keypair.fromSecretKey(Uint8Array.from(parsed));
    }
  } catch (e) {
    // Not JSON
  }

  throw new Error('Invalid private key format');
}

WebSocket Subscriptions

Real-time account monitoring for instant updates:

private setupAccountSubscriptions(): void {
  if (!this.keypair) return;

  // Subscribe to account changes
  const accountSub = this.connection.onAccountChange(
    this.keypair.publicKey,
    (accountInfo) => {
      elizaLogger.info('Account balance changed:', {
        lamports: accountInfo.lamports,
        sol: accountInfo.lamports / LAMPORTS_PER_SOL
      });
      
      // Trigger portfolio refresh
      this.fetchPortfolioData();
    },
    'confirmed'
  );
  
  this.subscriptions.push(accountSub);

  // Subscribe to token accounts
  this.subscribeToTokenAccounts();
}

private async subscribeToTokenAccounts(): Promise<void> {
  const tokenAccounts = await this.connection.getParsedTokenAccountsByOwner(
    this.keypair.publicKey,
    { programId: TOKEN_PROGRAM_ID }
  );

  tokenAccounts.value.forEach(({ pubkey }) => {
    const sub = this.connection.onAccountChange(
      pubkey,
      () => {
        elizaLogger.info('Token balance changed');
        this.fetchPortfolioData();
      },
      'confirmed'
    );
    
    this.subscriptions.push(sub);
  });
}

Portfolio Data Management

Efficient caching and data fetching:

interface PortfolioData {
  totalUsd: number;
  totalSol: number;
  solPrice: number;
  tokens: TokenBalance[];
  lastUpdated: number;
}

private async fetchPortfolioData(): Promise<PortfolioData> {
  const cacheKey = 'portfolio_data';
  const cached = this.cache.get(cacheKey);
  
  // Return cached data if fresh (2 minutes)
  if (cached && Date.now() - cached.timestamp < 120000) {
    return cached.data;
  }

  try {
    // Fetch from Birdeye API
    const response = await fetch(
      `https://api.birdeye.so/v1/wallet/portfolio?wallet=${this.keypair.publicKey.toBase58()}`,
      {
        headers: {
          'X-API-KEY': this.runtime.getSetting('BIRDEYE_API_KEY')
        }
      }
    );

    const data = await response.json();
    
    // Process and cache
    const portfolioData = this.processPortfolioData(data);
    this.cache.set(cacheKey, {
      data: portfolioData,
      timestamp: Date.now()
    });

    return portfolioData;
  } catch (error) {
    elizaLogger.error('Failed to fetch portfolio data:', error);
    return cached?.data || this.getEmptyPortfolio();
  }
}

Transaction Building

Optimized transaction construction with priority fees:

async function buildTransferTransaction(
  connection: Connection,
  sender: PublicKey,
  recipient: PublicKey,
  amount: number,
  token?: string
): Promise<Transaction> {
  const transaction = new Transaction();
  
  // Add priority fee for faster processing
  const priorityFee = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 1000 // 0.001 SOL per compute unit
  });
  transaction.add(priorityFee);

  if (!token || token.toUpperCase() === 'SOL') {
    // Native SOL transfer
    transaction.add(
      SystemProgram.transfer({
        fromPubkey: sender,
        toPubkey: recipient,
        lamports: amount * LAMPORTS_PER_SOL
      })
    );
  } else {
    // SPL token transfer
    const mint = await resolveTokenMint(connection, token);
    const senderAta = await getAssociatedTokenAddress(mint, sender);
    const recipientAta = await getAssociatedTokenAddress(mint, recipient);
    
    // Check if recipient ATA exists
    const recipientAccount = await connection.getAccountInfo(recipientAta);
    if (!recipientAccount) {
      // Create ATA for recipient
      transaction.add(
        createAssociatedTokenAccountInstruction(
          sender,
          recipientAta,
          recipient,
          mint
        )
      );
    }
    
    // Add transfer instruction
    transaction.add(
      createTransferInstruction(
        senderAta,
        recipientAta,
        sender,
        amount * Math.pow(10, await getTokenDecimals(connection, mint))
      )
    );
  }

  // Get latest blockhash
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
  transaction.recentBlockhash = blockhash;
  transaction.lastValidBlockHeight = lastValidBlockHeight;
  transaction.feePayer = sender;

  return transaction;
}

Token Resolution

Intelligent token symbol to mint address resolution:

async function resolveTokenMint(
  connection: Connection,
  tokenIdentifier: string
): Promise<PublicKey> {
  // Check if it's already a valid public key
  try {
    const pubkey = new PublicKey(tokenIdentifier);
    // Verify it's a token mint
    const accountInfo = await connection.getAccountInfo(pubkey);
    if (accountInfo?.owner.equals(TOKEN_PROGRAM_ID)) {
      return pubkey;
    }
  } catch (e) {
    // Not a valid public key, continue
  }

  // Common token mappings
  const commonTokens: Record<string, string> = {
    'USDC': 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
    'USDT': 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
    'BONK': 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
    'RAY': '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R',
    'JTO': 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL',
    // Add more as needed
  };

  const upperToken = tokenIdentifier.toUpperCase();
  if (commonTokens[upperToken]) {
    return new PublicKey(commonTokens[upperToken]);
  }

  // Try to fetch from token list or registry
  throw new Error(`Unknown token: ${tokenIdentifier}`);
}

Jupiter Integration

Advanced swap execution with route optimization:

interface JupiterSwapParams {
  inputMint: PublicKey;
  outputMint: PublicKey;
  amount: number;
  slippageBps: number;
  userPublicKey: PublicKey;
}

async function getJupiterQuote(params: JupiterSwapParams): Promise<QuoteResponse> {
  const url = new URL('https://quote-api.jup.ag/v6/quote');
  url.searchParams.append('inputMint', params.inputMint.toBase58());
  url.searchParams.append('outputMint', params.outputMint.toBase58());
  url.searchParams.append('amount', params.amount.toString());
  url.searchParams.append('slippageBps', params.slippageBps.toString());
  url.searchParams.append('onlyDirectRoutes', 'false');
  url.searchParams.append('asLegacyTransaction', 'false');

  const response = await fetch(url.toString());
  if (!response.ok) {
    throw new Error(`Jupiter quote failed: ${response.statusText}`);
  }

  return response.json();
}

async function executeJupiterSwap(
  connection: Connection,
  wallet: Wallet,
  quote: QuoteResponse
): Promise<{ signature: string }> {
  // Get serialized transaction from Jupiter
  const swapResponse = await fetch('https://quote-api.jup.ag/v6/swap', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      quoteResponse: quote,
      userPublicKey: wallet.publicKey.toBase58(),
      wrapAndUnwrapSol: true,
      prioritizationFeeLamports: 'auto'
    })
  });

  const { swapTransaction } = await swapResponse.json();
  
  // Deserialize and sign
  const transaction = VersionedTransaction.deserialize(
    Buffer.from(swapTransaction, 'base64')
  );
  transaction.sign([wallet.payer]);

  // Send with confirmation
  const signature = await connection.sendTransaction(transaction, {
    skipPreflight: false,
    maxRetries: 3
  });

  // Wait for confirmation
  const confirmation = await connection.confirmTransaction({
    signature,
    blockhash: transaction.message.recentBlockhash,
    lastValidBlockHeight: transaction.message.lastValidBlockHeight
  });

  if (confirmation.value.err) {
    throw new Error(`Swap failed: ${confirmation.value.err}`);
  }

  return { signature };
}

Error Handling

Comprehensive error handling with retry logic:

export async function withRetry<T>(
  operation: () => Promise<T>,
  options: {
    maxAttempts?: number;
    delay?: number;
    backoff?: number;
    onError?: (error: Error, attempt: number) => void;
  } = {}
): Promise<T> {
  const {
    maxAttempts = 3,
    delay = 1000,
    backoff = 2,
    onError
  } = options;

  let lastError: Error;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      onError?.(error, attempt);

      if (attempt < maxAttempts) {
        const waitTime = delay * Math.pow(backoff, attempt - 1);
        elizaLogger.warn(`Attempt ${attempt} failed, retrying in ${waitTime}ms`, {
          error: error.message
        });
        await new Promise(resolve => setTimeout(resolve, waitTime));
      }
    }
  }

  throw lastError;
}

// Usage
const result = await withRetry(
  () => connection.sendTransaction(transaction),
  {
    maxAttempts: 3,
    onError: (error, attempt) => {
      if (error.message.includes('blockhash not found')) {
        // Refresh blockhash
        transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
      }
    }
  }
);

Performance Optimizations

Connection Pooling

class ConnectionPool {
  private connections: Connection[] = [];
  private currentIndex = 0;

  constructor(rpcUrls: string[], config?: ConnectionConfig) {
    this.connections = rpcUrls.map(url => new Connection(url, config));
  }

  getConnection(): Connection {
    const connection = this.connections[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.connections.length;
    return connection;
  }

  async healthCheck(): Promise<void> {
    const checks = this.connections.map(async (conn, index) => {
      try {
        await conn.getVersion();
        return { index, healthy: true };
      } catch (error) {
        return { index, healthy: false, error };
      }
    });

    const results = await Promise.all(checks);
    const unhealthy = results.filter(r => !r.healthy);
    
    if (unhealthy.length > 0) {
      elizaLogger.warn('Unhealthy connections:', unhealthy);
    }
  }
}

Batch Operations

async function batchGetMultipleAccounts(
  connection: Connection,
  publicKeys: PublicKey[]
): Promise<(AccountInfo<Buffer> | null)[]> {
  const BATCH_SIZE = 100;
  const results: (AccountInfo<Buffer> | null)[] = [];

  for (let i = 0; i < publicKeys.length; i += BATCH_SIZE) {
    const batch = publicKeys.slice(i, i + BATCH_SIZE);
    const batchResults = await connection.getMultipleAccountsInfo(batch);
    results.push(...batchResults);
  }

  return results;
}

Security Considerations

  1. Private Key Security

    • Never log or expose private keys
    • Support multiple secure key formats
    • Use environment variables only
  2. Transaction Validation

    • Always simulate before sending
    • Verify recipient addresses
    • Check token mint addresses
  3. Slippage Protection

    • Default 1% slippage
    • Maximum 5% slippage
    • User confirmation for high slippage
  4. Rate Limiting

    • Implement request throttling
    • Cache frequently accessed data
    • Use WebSocket for real-time data

Monitoring & Logging

The plugin provides detailed logging for debugging and monitoring:

// Transaction lifecycle
elizaLogger.info('Transfer initiated', { amount, token, recipient });
elizaLogger.debug('Transaction built', { instructions: tx.instructions.length });
elizaLogger.info('Transaction sent', { signature });
elizaLogger.info('Transaction confirmed', { signature, slot });

// Performance metrics
elizaLogger.debug('RPC latency', { method, duration });
elizaLogger.debug('Cache hit rate', { hits, misses, ratio });

// Error tracking
elizaLogger.error('Transaction failed', { error, context });
elizaLogger.warn('Retry attempt', { attempt, maxAttempts });