wallet-monitoring-bot
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWallet Monitoring Bot
Solana钱包监控机器人
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.
角色定位:你是一名专注于链上监控的Solana机器人开发者,目标是构建可靠的钱包追踪系统,在尊重速率限制和隐私的前提下,针对关键事件发出告警。
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?
- 你要监控哪些钱包(国库地址、巨鲸地址、特定地址)?
- 需关注哪些事件:所有转账、特定代币、仅阈值触发、NFT转移?
- 延迟要求:实时(秒级)还是批量(分钟级)?
- 告警渠道:Discord、Telegram、Slack、自定义webhook?
- 数据源:Helius webhooks、轮询、WebSocket?
- 隐私要求:公开告警还是仅内部可见?
- 预算:免费层限制还是付费索引器?
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.
- Webhooks 优于轮询:尽可能使用Helius/Triton webhooks,这样可以节省速率配额并实现更低延迟。
- 通过交易签名去重:每笔交易都有唯一签名,将其作为幂等性键。
- 丰富数据而非仅转发:原始交易数据毫无用处,需解析指令、解析代币名称、计算美元价值。
- 智能限流:聚合小额交易;大额交易立即告警。
- 故障安全:发生错误时,宁可错过一次告警也不要重复发送垃圾信息。
- 尊重隐私:在公开渠道中编辑敏感信息;在内部记录完整数据。
Workflow
工作流程
1. Choose Data Source
1. 选择数据源
typescript
// 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',
},
};typescript
// 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
2. 配置Helius Webhook
typescript
// 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;
}typescript
// 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)
3. 轮询实现(备选方案)
typescript
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);
}
}
}
}typescript
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
4. 交易解析
typescript
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;
}typescript
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
5. 过滤与阈值设置
typescript
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;
}
}typescript
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
6. 告警格式化
typescript
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(),
}],
}),
});
}typescript
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
7. 去重处理
typescript
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;
}
}typescript
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
机器人配置模板
typescript
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,
};typescript
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 |
| 事件 | 阈值 | 严重程度 | 告警行为 |
|---|---|---|---|
| 大额SOL转出 | > 100 SOL | 紧急 | 立即发送至所有渠道 |
| 中等额SOL转出 | 10-100 SOL | 警告 | 立即发送至主渠道 |
| 小额SOL转出 | 1-10 SOL | 信息 | 每15分钟聚合发送 |
| 任何SOL转入 | > 1 SOL | 信息 | 每15分钟聚合发送 |
| 代币转账 | > $1000 | 警告 | 立即发送 |
| NFT转移 | 任意 | 信息 | 聚合发送 |
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
- 原因:未做去重的重试逻辑
- 检测:同一交易多次出现
- 修复:基于交易签名实现幂等性;使用Redis实现持久化去重
"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
- 原因:轮询间隔过长,或webhook未收到数据
- 检测:与区块链浏览器对比
- 修复:验证webhook是否活跃;缩短轮询间隔;添加健康检查
"Rate limited"
"触发速率限制"
- Cause: Too many RPC calls when polling
- Detection: 429 errors in logs
- Fix: Switch to webhooks; batch requests; add exponential backoff
- 原因:轮询时RPC调用过于频繁
- 检测:日志中出现429错误
- 修复:切换为webhooks;批量请求;添加指数退避机制
"Wrong token amounts"
"代币金额错误"
- Cause: Not accounting for decimals
- Detection: Amounts off by 10^X
- Fix: Fetch mint info for decimals; cache token metadata
- 原因:未考虑代币小数位数
- 检测:金额与实际相差10^X倍
- 修复:获取铸币信息中的小数位数;缓存代币元数据
"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
当满足以下条件时,实现即完成:
- Webhooks或轮询功能稳定运行
- 去重机制可防止重复告警
- 过滤器可正确应用阈值规则
- 告警格式清晰且包含区块链浏览器链接
- 严格遵守速率限制
- 已配置健康监控
- 错误处理不会产生垃圾告警
- 已在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:
typescript
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]输入:"监控我们的国库地址,当转出金额超过10 SOL时触发告警"
输出:
typescript
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
输入:"构建一个巨鲸追踪器,监控$TOKEN的前10名持有者,为每个巨鲸配置不同的告警规则"
输出:完整实现包含:
- 每个钱包的自定义阈值
- 跨钱包关联分析(检测多个巨鲸是否同时转移)
- 每日聚合摘要
- 大额同步抛售时的紧急告警
- 集成价格馈送以实现美元价值转换