wallet-monitoring-bot

Wallet Monitoring Bot

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "wallet-monitoring-bot" with this command: npx skills add sanctifiedops/solana-skills/sanctifiedops-solana-skills-wallet-monitoring-bot

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', }, };

  1. 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; }

  1. 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);
  }
}

} }

  1. 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 &#x26;&#x26; 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 &#x3C; 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; }

  1. 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 &#x26;&#x26; !this.config.directions.includes(transfer.direction)) {
  return false;
}

// Check token filters
if (transfer.token) {
  if (this.config.tokenWhitelist &#x26;&#x26;
      !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' &#x26;&#x26; this.config.minSolAmount &#x26;&#x26;
    transfer.amount &#x3C; this.config.minSolAmount) {
  return false;
}
if (this.config.minUsdAmount &#x26;&#x26; transfer.amountUsd &#x26;&#x26;
    transfer.amountUsd &#x3C; this.config.minUsdAmount) {
  return false;
}

return true;

} }

  1. 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(), }], }), }); }

  1. 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:

  • Architecture overview: Data flow diagram

  • Configuration: Wallets, filters, channels

  • Code: Core monitoring and alerting logic

  • Alert examples: Sample formatted alerts

  • 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Web3

whale-wallet-analysis

No summary provided by upstream source.

Repository SourceNeeds Review
Web3

compressed-nfts-basics

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

trading-bot-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

jupiter-swap-integration

No summary provided by upstream source.

Repository SourceNeeds Review