skills/sanctifiedops/solana-skills/wallet-monitoring-bot

wallet-monitoring-bot

SKILL.md

Wallet Monitoring Bot

Role framing: You are a Solana bot builder specializing in on-chain monitoring. Your goal is to build reliable wallet tracking systems that alert on meaningful events while respecting rate limits and privacy.

Initial Assessment

  • What wallets are you monitoring (treasury, whales, specific addresses)?
  • What events matter: all transfers, specific tokens, thresholds only, NFT moves?
  • Latency requirements: real-time (seconds) or batch (minutes)?
  • Alert channels: Discord, Telegram, Slack, custom webhook?
  • Data source: Helius webhooks, polling, WebSocket?
  • Privacy requirements: public alerts or internal only?
  • Budget: free tier limitations or paid indexer?

Core Principles

  • Webhooks > Polling: Use Helius/Triton webhooks when possible. Saves rate limits and provides lower latency.
  • Deduplicate by signature: Every transaction has a unique signature. Use it as idempotency key.
  • Enrich, don't just relay: Raw tx data is useless. Parse instructions, resolve token names, calculate USD values.
  • Throttle intelligently: Aggregate small events; immediately alert on large ones.
  • Fail safe: On error, miss an alert rather than spam duplicates.
  • Respect privacy: Redact sensitive info for public channels; log full data internally.

Workflow

1. Choose Data Source

// Option A: Helius Webhooks (Recommended)
// - Real-time, low latency
// - No rate limit concerns for receiving
// - Requires Helius account

// Option B: Polling with getSignaturesForAddress
// - Works with any RPC
// - Higher latency (poll interval)
// - Rate limit sensitive

// Option C: WebSocket (accountSubscribe)
// - Real-time for account balance changes
// - Doesn't capture full tx details
// - Connection management complexity

const DATA_SOURCES = {
  heliusWebhook: {
    latency: '1-3 seconds',
    rateLimit: 'None (receiving)',
    setup: 'Medium',
    cost: 'Free tier: 10 webhooks, Paid: unlimited',
  },
  polling: {
    latency: 'Poll interval (5-60s typical)',
    rateLimit: 'Subject to RPC limits',
    setup: 'Easy',
    cost: 'RPC costs',
  },
  websocket: {
    latency: 'Sub-second',
    rateLimit: 'Connection limits',
    setup: 'Complex (reconnection logic)',
    cost: 'RPC costs',
  },
};

2. Setup Helius Webhook

// Create webhook via Helius API
async function createHeliusWebhook(
  apiKey: string,
  walletAddresses: string[],
  webhookUrl: string
): Promise<string> {
  const response = await fetch('https://api.helius.xyz/v0/webhooks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      webhookURL: webhookUrl,
      transactionTypes: ['TRANSFER', 'SWAP', 'NFT_SALE'],
      accountAddresses: walletAddresses,
      webhookType: 'enhanced', // Parsed transaction data
      authHeader: 'X-Webhook-Secret: your-secret', // Optional
    }),
  });

  const { webhookID } = await response.json();
  return webhookID;
}

// Webhook payload structure (enhanced mode)
interface HeliusWebhookPayload {
  type: string;
  fee: number;
  feePayer: string;
  signature: string;
  slot: number;
  timestamp: number;
  nativeTransfers: NativeTransfer[];
  tokenTransfers: TokenTransfer[];
  description: string;
  source: string;
}

3. Polling Implementation (Alternative)

import { Connection, PublicKey } from '@solana/web3.js';

class WalletPoller {
  private connection: Connection;
  private lastSignatures: Map<string, string> = new Map();
  private pollInterval: number;

  constructor(rpcUrl: string, pollIntervalMs: number = 10000) {
    this.connection = new Connection(rpcUrl);
    this.pollInterval = pollIntervalMs;
  }

  async startPolling(
    wallets: string[],
    onTransaction: (wallet: string, tx: ParsedTransaction) => void
  ) {
    // Initial fetch to set baseline
    for (const wallet of wallets) {
      const sigs = await this.connection.getSignaturesForAddress(
        new PublicKey(wallet),
        { limit: 1 }
      );
      if (sigs.length > 0) {
        this.lastSignatures.set(wallet, sigs[0].signature);
      }
    }

    // Poll loop
    setInterval(async () => {
      for (const wallet of wallets) {
        try {
          await this.checkWallet(wallet, onTransaction);
        } catch (error) {
          console.error(`Error polling ${wallet}:`, error);
        }
      }
    }, this.pollInterval);
  }

  private async checkWallet(
    wallet: string,
    onTransaction: (wallet: string, tx: ParsedTransaction) => void
  ) {
    const lastSig = this.lastSignatures.get(wallet);

    const sigs = await this.connection.getSignaturesForAddress(
      new PublicKey(wallet),
      {
        until: lastSig,
        limit: 20,
      }
    );

    if (sigs.length === 0) return;

    // Update last seen
    this.lastSignatures.set(wallet, sigs[0].signature);

    // Process new transactions (oldest first)
    for (const sig of sigs.reverse()) {
      const tx = await this.connection.getParsedTransaction(sig.signature, {
        maxSupportedTransactionVersion: 0,
      });

      if (tx) {
        onTransaction(wallet, tx);
      }
    }
  }
}

4. Transaction Parsing

interface ParsedTransfer {
  signature: string;
  timestamp: number;
  type: 'SOL' | 'TOKEN' | 'NFT';
  direction: 'IN' | 'OUT';
  amount: number;
  amountUsd?: number;
  token?: {
    mint: string;
    symbol: string;
    decimals: number;
  };
  from: string;
  to: string;
  fee: number;
}

function parseTransaction(
  watchedWallet: string,
  tx: ParsedTransactionWithMeta
): ParsedTransfer[] {
  const transfers: ParsedTransfer[] = [];

  // Parse native SOL transfers
  const preBalances = tx.meta?.preBalances || [];
  const postBalances = tx.meta?.postBalances || [];
  const accounts = tx.transaction.message.accountKeys;

  for (let i = 0; i < accounts.length; i++) {
    const account = accounts[i].pubkey.toString();
    const change = (postBalances[i] - preBalances[i]) / 1e9;

    if (account === watchedWallet && Math.abs(change) > 0.0001) {
      transfers.push({
        signature: tx.transaction.signatures[0],
        timestamp: tx.blockTime || 0,
        type: 'SOL',
        direction: change > 0 ? 'IN' : 'OUT',
        amount: Math.abs(change),
        from: change < 0 ? watchedWallet : 'unknown',
        to: change > 0 ? watchedWallet : 'unknown',
        fee: tx.meta?.fee || 0,
      });
    }
  }

  // Parse token transfers
  const tokenTransfers = tx.meta?.postTokenBalances || [];
  // ... additional parsing logic

  return transfers;
}

5. Filtering and Thresholds

interface FilterConfig {
  // Amount thresholds (in USD or native units)
  minSolAmount?: number;
  minUsdAmount?: number;

  // Token filters
  tokenWhitelist?: string[]; // Only these tokens
  tokenBlacklist?: string[]; // Ignore these tokens

  // Direction filters
  directions?: ('IN' | 'OUT')[];

  // Aggregation
  aggregateWindow?: number; // ms to aggregate small transfers
  aggregateThreshold?: number; // Below this, aggregate

  // Cooldown
  cooldownPerWallet?: number; // ms between alerts for same wallet
}

class TransferFilter {
  private config: FilterConfig;
  private lastAlert: Map<string, number> = new Map();
  private pendingAggregates: Map<string, ParsedTransfer[]> = new Map();

  constructor(config: FilterConfig) {
    this.config = config;
  }

  shouldAlert(wallet: string, transfer: ParsedTransfer): boolean {
    // Check cooldown
    const lastTime = this.lastAlert.get(wallet) || 0;
    if (Date.now() - lastTime < (this.config.cooldownPerWallet || 0)) {
      return false;
    }

    // Check direction
    if (this.config.directions && !this.config.directions.includes(transfer.direction)) {
      return false;
    }

    // Check token filters
    if (transfer.token) {
      if (this.config.tokenWhitelist &&
          !this.config.tokenWhitelist.includes(transfer.token.mint)) {
        return false;
      }
      if (this.config.tokenBlacklist?.includes(transfer.token.mint)) {
        return false;
      }
    }

    // Check amount thresholds
    if (transfer.type === 'SOL' && this.config.minSolAmount &&
        transfer.amount < this.config.minSolAmount) {
      return false;
    }
    if (this.config.minUsdAmount && transfer.amountUsd &&
        transfer.amountUsd < this.config.minUsdAmount) {
      return false;
    }

    return true;
  }
}

6. Alert Formatting

interface AlertMessage {
  title: string;
  description: string;
  fields: { name: string; value: string; inline?: boolean }[];
  color: number;
  url?: string;
}

function formatDiscordAlert(
  wallet: string,
  transfer: ParsedTransfer,
  walletLabel?: string
): AlertMessage {
  const direction = transfer.direction === 'IN' ? '📥 Received' : '📤 Sent';
  const emoji = transfer.direction === 'IN' ? '🟢' : '🔴';

  const amount = transfer.type === 'SOL'
    ? `${transfer.amount.toFixed(4)} SOL`
    : `${transfer.amount.toLocaleString()} ${transfer.token?.symbol || 'tokens'}`;

  const usdValue = transfer.amountUsd
    ? ` (~$${transfer.amountUsd.toLocaleString()})`
    : '';

  return {
    title: `${emoji} ${direction}${walletLabel ? ` - ${walletLabel}` : ''}`,
    description: `${amount}${usdValue}`,
    fields: [
      {
        name: 'Wallet',
        value: `\`${wallet.slice(0, 4)}...${wallet.slice(-4)}\``,
        inline: true,
      },
      {
        name: transfer.direction === 'IN' ? 'From' : 'To',
        value: `\`${(transfer.direction === 'IN' ? transfer.from : transfer.to).slice(0, 8)}...\``,
        inline: true,
      },
      {
        name: 'Time',
        value: `<t:${transfer.timestamp}:R>`,
        inline: true,
      },
    ],
    color: transfer.direction === 'IN' ? 0x00ff00 : 0xff6b6b,
    url: `https://solscan.io/tx/${transfer.signature}`,
  };
}

// Send to Discord
async function sendDiscordAlert(
  webhookUrl: string,
  alert: AlertMessage
): Promise<void> {
  await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      embeds: [{
        title: alert.title,
        description: alert.description,
        fields: alert.fields,
        color: alert.color,
        url: alert.url,
        timestamp: new Date().toISOString(),
      }],
    }),
  });
}

7. Deduplication

class SignatureDeduplicator {
  private seen: Set<string> = new Set();
  private maxSize: number;
  private cleanupThreshold: number;

  constructor(maxSize: number = 10000) {
    this.maxSize = maxSize;
    this.cleanupThreshold = maxSize * 0.8;
  }

  isDuplicate(signature: string): boolean {
    if (this.seen.has(signature)) {
      return true;
    }

    // Add to seen set
    this.seen.add(signature);

    // Cleanup if too large (simple approach: clear half)
    if (this.seen.size > this.maxSize) {
      const toKeep = Array.from(this.seen).slice(-this.cleanupThreshold);
      this.seen = new Set(toKeep);
    }

    return false;
  }
}

// For persistent deduplication, use Redis
import Redis from 'ioredis';

class RedisDeduplicator {
  private redis: Redis;
  private keyPrefix: string;
  private ttlSeconds: number;

  constructor(redisUrl: string, ttlSeconds: number = 86400) {
    this.redis = new Redis(redisUrl);
    this.keyPrefix = 'tx:seen:';
    this.ttlSeconds = ttlSeconds;
  }

  async isDuplicate(signature: string): Promise<boolean> {
    const key = `${this.keyPrefix}${signature}`;
    const exists = await this.redis.exists(key);

    if (exists) {
      return true;
    }

    await this.redis.setex(key, this.ttlSeconds, '1');
    return false;
  }
}

Templates / Playbooks

Bot Configuration Template

interface BotConfig {
  // Wallets to monitor
  wallets: {
    address: string;
    label: string;
    filters?: FilterConfig;
  }[];

  // Data source
  dataSource: 'helius' | 'polling' | 'websocket';
  heliusApiKey?: string;
  rpcUrl?: string;
  pollIntervalMs?: number;

  // Alerting
  alertChannels: {
    type: 'discord' | 'telegram' | 'slack' | 'webhook';
    url: string;
    minSeverity?: 'info' | 'warning' | 'critical';
  }[];

  // Defaults
  defaultFilters: FilterConfig;

  // Operations
  healthCheckInterval: number;
  metricsEnabled: boolean;
}

const exampleConfig: BotConfig = {
  wallets: [
    {
      address: 'Treasury...xyz',
      label: 'Project Treasury',
      filters: { minUsdAmount: 1000 },
    },
    {
      address: 'Whale...abc',
      label: 'Known Whale',
      filters: { directions: ['OUT'] },
    },
  ],
  dataSource: 'helius',
  heliusApiKey: process.env.HELIUS_API_KEY,
  alertChannels: [
    {
      type: 'discord',
      url: process.env.DISCORD_WEBHOOK,
    },
  ],
  defaultFilters: {
    minSolAmount: 1,
    minUsdAmount: 100,
    cooldownPerWallet: 60000,
  },
  healthCheckInterval: 60000,
  metricsEnabled: true,
};

Alert Severity Matrix

Event Threshold Severity Alert Behavior
Large SOL out > 100 SOL Critical Immediate, all channels
Medium SOL out 10-100 SOL Warning Immediate, primary channel
Small SOL out 1-10 SOL Info Batch every 15 min
Any SOL in > 1 SOL Info Batch every 15 min
Token transfer > $1000 Warning Immediate
NFT move Any Info Batch

Common Failure Modes + Debugging

"Duplicate alerts"

  • Cause: Retry logic without deduplication
  • Detection: Same tx appearing multiple times
  • Fix: Implement signature-based idempotency; use Redis for persistence

"Missing transactions"

  • Cause: Poll interval too long, or webhook not receiving
  • Detection: Compare with explorer
  • Fix: Verify webhook is active; reduce poll interval; add health check

"Rate limited"

  • Cause: Too many RPC calls when polling
  • Detection: 429 errors in logs
  • Fix: Switch to webhooks; batch requests; add exponential backoff

"Wrong token amounts"

  • Cause: Not accounting for decimals
  • Detection: Amounts off by 10^X
  • Fix: Fetch mint info for decimals; cache token metadata

"Bot crashed but no alert"

  • Cause: No health monitoring
  • Detection: Silent failure
  • Fix: Add health check endpoint; alert on missed heartbeat

Quality Bar / Validation

Implementation is complete when:

  • Webhooks or polling working reliably
  • Deduplication prevents duplicate alerts
  • Filters correctly apply thresholds
  • Alert formatting is clear and includes links
  • Rate limits respected
  • Health monitoring in place
  • Error handling doesn't spam alerts
  • Tested with real transactions on devnet

Output Format

Provide:

  1. Architecture overview: Data flow diagram
  2. Configuration: Wallets, filters, channels
  3. Code: Core monitoring and alerting logic
  4. Alert examples: Sample formatted alerts
  5. Operational runbook: Health checks, debugging steps

Examples

Simple Example: Treasury Monitor

Input: "Monitor our treasury for any outgoing transfer > 10 SOL"

Output:

const treasuryMonitor = new WalletMonitor({
  wallets: [{
    address: 'TreasuryAddressHere',
    label: 'Project Treasury',
  }],
  filters: {
    directions: ['OUT'],
    minSolAmount: 10,
  },
  alertChannel: {
    type: 'discord',
    url: process.env.DISCORD_TREASURY_WEBHOOK,
  },
});

// Alert format:
// 🔴 Sent - Project Treasury
// 25.5 SOL (~$2,550)
// Wallet: Trea...xyz
// To: 7xK8...abc
// Time: 2 minutes ago
// [View on Solscan]

Complex Example: Multi-Wallet Whale Tracker

Input: "Build a whale tracker that monitors top 10 holders of $TOKEN with different alert rules per whale"

Output: See full implementation with:

  • Per-wallet custom thresholds
  • Cross-wallet correlation (detect if multiple whales moving together)
  • Aggregated daily summaries
  • Critical alerts for large simultaneous sells
  • Integration with price feed for USD conversion
Weekly Installs
2
First Seen
2 days ago
Installed on
opencode2
codex2
claude-code2
antigravity2
gemini-cli2
windsurf1