��# watch-tower

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.
What it does
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/--to flags 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
Quick start
Installation
go install github.com/devblac/watch-tower/cmd/watch-tower@latest
Or download a pre-built binary from the releases page.
Your first alert (5 minutes)
- 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 --once
That's it. If there's a large USDC transfer in the last 1000 blocks, you'll get a Slack notification.
Configuration
Sources
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 files
Algorand source:
sources:
- id: algo_mainnet
type: algorand
algod_url: ${ALGOD_URL}
indexer_url: ${ALGO_INDEXER_URL}
start_round: "latest-10000"
Rules
Rules define what to watch for and what to do when it happens.
Match types:
log: EVM event logs (requires contract and event)
app_call: Algorand application calls (requires app_id)
asset_transfer: Algorand ASA transfers
Predicates:
Simple expressions to filter events:
value >= 1000
sender == "0x123..."
sender in addr1,addr2,addr3
memo contains "alert"
value >= wei(1e18) # helper for wei amounts
amount >= 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 second
Sinks
Sinks 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 JSON
Template 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
Reorgs and confirmations
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 and dry-run
Replay historical blocks:
watch-tower run -c config.yaml --from 18000000 --to 18001000
Dry-run (no alerts sent):
watch-tower run -c config.yaml --dry-run --once
Check current state:
watch-tower state
Export data:
watch-tower export alerts --format json
watch-tower export cursors --format csv
Health and metrics
Health endpoint:
watch-tower run -c config.yaml --health :8080
# Check: curl http://localhost:8080/healthz
Prometheus metrics:
watch-tower run -c config.yaml --metrics :9090
# Scrape: curl http://localhost:9090/metrics
Metrics include:
watch_tower_blocks_processed_total
watch_tower_alerts_sent_total
watch_tower_alerts_dropped_total (dedupe/rate-limit)
watch_tower_errors_total
Examples
See the examples/ directory for complete working configurations:
examples/evm_usdc_whale/ - Monitor large USDC transfers on Ethereum
examples/algo_app_watch/ - Watch Algorand application calls
Each example includes a config file, .env.example, and a README with setup instructions.
CI integration
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-run
The --dry-run flag processes events but doesn't send alerts, perfect for validating rules in CI.
Development
make lint # Run linters
make test # Run tests
make build # Build binary
How it works
- 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.
Security
- 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.
Limitations
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.
Contributing
Contributions welcome! See CONTRIBUTING.md for guidelines.
License
MIT see LICENSE.