A simple CLI tool for monitoring EVM and Algorand chains. Set up rules in YAML, get alerts when things happen. No SaaS, no complexity—just a single binary that does what you need.
Watch-tower monitors blockchain events and sends alerts when your rules match. It handles reorgs safely, deduplicates alerts, and works great in CI. Think of it as a lightweight alternative to running your own monitoring infrastructure.
Key features:
- Reorg-safe: Uses confirmations and stores block hashes to detect and handle chain reorganizations
- Exactly-once alerts: SQLite ledger ensures you don't get duplicate notifications
- CI-friendly:
--dry-run,--once, and--from/--toflags make it perfect for testing - Cross-chain: Works with EVM chains (Ethereum, Polygon, etc.) and Algorand
- Simple config: YAML files with environment variable interpolation—no code required
go install github.com/devblac/watch-tower/cmd/watch-tower@latestOr download a pre-built binary from the releases page.
- Create a config file (
config.yaml):
version: 1
global:
db_path: "./watch_tower.db"
confirmations:
evm: 12
sources:
- id: mainnet
type: evm
rpc_url: ${EVM_RPC_URL}
start_block: "latest-1000"
rules:
- id: large_transfer
source: mainnet
match:
type: log
contract: "0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" # USDC
event: "Transfer(address,address,uint256)"
where:
- "value >= 1_000_000 * 1e6" # 1M USDC
sinks: ["slack"]
dedupe:
key: "txhash:logIndex"
ttl: "24h"
sinks:
- id: slack
type: slack
webhook_url: ${SLACK_WEBHOOK_URL}
template: "🚨 Large USDC transfer: {{txhash}}"- Set your environment variables:
export EVM_RPC_URL="https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"- Validate and run:
watch-tower validate -c config.yaml
watch-tower run -c config.yaml --onceThat's it. If there's a large USDC transfer in the last 1000 blocks, you'll get a Slack notification.
Sources define which chains to monitor. You can have multiple sources (e.g., mainnet and testnet).
EVM source:
sources:
- id: mainnet
type: evm
rpc_url: ${EVM_RPC_URL}
start_block: "latest-5000" # or "12345678" for a specific block
abi_dirs: ["./abis"] # optional: directory with ABI JSON filesAlgorand source:
sources:
- id: algo_mainnet
type: algorand
algod_url: ${ALGOD_URL}
indexer_url: ${ALGO_INDEXER_URL}
start_round: "latest-10000"Rules define what to watch for and what to do when it happens.
Match types:
log: EVM event logs (requirescontractandevent)app_call: Algorand application calls (requiresapp_id)asset_transfer: Algorand ASA transfers
Predicates: Simple expressions to filter events:
value >= 1000sender == "0x123..."sender in addr1,addr2,addr3memo contains "alert"value >= wei(1e18)# helper for wei amountsamount >= microAlgos(1e6)# helper for Algorand amounts
Example rule:
rules:
- id: whale_alert
source: mainnet
match:
type: log
contract: "0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
event: "Transfer(address,address,uint256)"
where:
- "value >= 1_000_000 * 1e6"
- "to != 0x0000000000000000000000000000000000000000" # exclude burns
sinks: ["slack", "webhook"]
dedupe:
key: "txhash:logIndex"
ttl: "24h"
rate_limit: # optional: limit alerts per rule
capacity: 10
rate: 1 # 1 alert per secondSinks are where alerts go. You can send to multiple sinks per rule.
Slack:
sinks:
- id: slack
type: slack
webhook_url: ${SLACK_WEBHOOK_URL}
template: "Alert: {{rule_id}} - {{txhash}}"Microsoft Teams:
sinks:
- id: teams
type: teams
webhook_url: ${TEAMS_WEBHOOK_URL}
template: "{{pretty_json}}"Generic webhook:
sinks:
- id: webhook
type: webhook
url: ${WEBHOOK_URL}
method: POST
template: "{{. | toJson}}" # full event as JSONTemplate variables:
{{rule_id}}- Rule identifier{{chain}}- Chain name (evm/algorand){{txhash}}- Transaction hash{{height}}- Block height/round{{pretty_json}}- Formatted event data{{short_addr addr}}- Shortened address- Any field from the event args
Watch-tower handles chain reorganizations automatically. Here's how it works:
-
Confirmations: You set how many confirmations to wait (e.g., 12 blocks for EVM). Watch-tower only processes blocks that are this many confirmations behind the chain tip.
-
Hash verification: Each block's parent hash is checked. If it doesn't match what we expect, a reorg is detected.
-
Rewind and replay: When a reorg is detected, watch-tower rewinds the cursor and reprocesses blocks. Your alerts stay accurate.
The confirmation count is per-chain in the global.confirmations section. More confirmations = more safety but more lag.
Replay historical blocks:
watch-tower run -c config.yaml --from 18000000 --to 18001000Dry-run (no alerts sent):
watch-tower run -c config.yaml --dry-run --onceCheck current state:
watch-tower stateExport data:
watch-tower export alerts --format json
watch-tower export cursors --format csvHealth endpoint:
watch-tower run -c config.yaml --health :8080
# Check: curl http://localhost:8080/healthzPrometheus metrics:
watch-tower run -c config.yaml --metrics :9090
# Scrape: curl http://localhost:9090/metricsMetrics include:
watch_tower_blocks_processed_totalwatch_tower_alerts_sent_totalwatch_tower_alerts_dropped_total(dedupe/rate-limit)watch_tower_errors_total
See the examples/ directory for complete working configurations:
examples/evm_usdc_whale/- Monitor large USDC transfers on Ethereumexamples/algo_app_watch/- Watch Algorand application calls
Each example includes a config file, .env.example, and a README with setup instructions.
Watch-tower is designed to work well in CI pipelines:
# .github/workflows/monitor.yml
- name: Check for alerts
run: |
watch-tower validate -c config.yaml
watch-tower run -c config.yaml --once --dry-runThe --dry-run flag processes events but doesn't send alerts, perfect for validating rules in CI.
make lint # Run linters
make test # Run tests
make build # Build binary- Scan blocks: Watch-tower polls each source for new blocks, respecting confirmation counts
- Match events: Events are matched against your rules using ABI decoding (EVM) or app call parsing (Algorand)
- Evaluate predicates: Simple expressions filter events (e.g.,
value > 1000) - Deduplicate: SQLite tracks what's been seen to prevent duplicate alerts
- Rate limit: Optional per-rule rate limiting prevents alert spam
- Send alerts: Templates are rendered and sent to configured sinks
All state is stored in a local SQLite database (default: ./watch_tower.db). This makes it easy to run multiple instances or move between machines.
- Secrets: All secrets must come from environment variables. The config validator will reject plain secrets in YAML files.
- Logging: Secrets are automatically redacted from logs (keys containing "token", "secret", "key", "password").
- HTTPS: Webhook sinks require HTTPS URLs.
See SECURITY.md for reporting vulnerabilities.
This is v0.1.0, so keep these in mind:
- Predicates are simple: No complex joins, arithmetic, or time windows yet
- Single binary: No plugins or extensions
- SQLite only: No Postgres option yet
- Basic sinks: Slack, Teams, and generic webhooks only
Check the roadmap in tasks.md for what's coming next.
Contributions welcome! See CONTRIBUTING.md for guidelines.
MIT — see LICENSE.