A dashboard and scanner for the Monad Validator Delegation Program (VDP) — tracking which addresses have been approved by the Monad Foundation, which have registered validators, and which are actively participating in the testnet.
Built by Prooflines.
The Monad Validator Delegation Program is the process by which the Monad Foundation approves addresses to run testnet validators. The flow is:
- Approval — The Foundation sends exactly 100,100 MON (a pure native transfer, no calldata) from its wallet
0xFA735CcA8424e4EF30980653BF9015331d9929dBto a recipient address. - Registration — The recipient sends 100,000 MON to the validator contract
0x0000000000000000000000000000000000001000using the function selector0xf145204c. The contract assigns a sequential validator ID. - Active operation — Active validators send heartbeat transactions (selector
0x791bdcf3) to the contract at roughly 1 per block (~2 seconds), spending 25 MON each time.
This tracker scans all three phases and presents the data in a single dashboard.
monad-vdp/
├── config.py # Environment-driven configuration
├── database.py # SQLite layer (WAL mode, thread-safe)
├── rpc.py # Async aiohttp JSON-RPC client (rate-limited, retries)
├── scanner.py # Binary-search scanner + activity checker
├── web.py # FastAPI dashboard (port 8081)
├── bot_commands.py # Telegram bot extensions
├── templates/
│ └── index.html # Dark-theme dashboard (Chart.js)
├── monad-vdp-web.service
└── monad-vdp-scanner.service
Two independent systemd services run in parallel:
- monad-vdp-scanner — scans every 5 minutes, writes to SQLite
- monad-vdp-web — serves the FastAPI dashboard, reads from SQLite
The Monad testnet RPC (https://testnet-rpc.monad.xyz) has a quirk: eth_getLogs with an address filter times out (413 response) even for short ranges, because the validator heartbeat events flood the log index. The scanner works entirely without eth_getLogs.
eth_getTransactionCount(foundation, block) is fast and works for the last ~5 million blocks. The scanner binary-searches the Foundation wallet's nonce to find the exact block for each of its 323 outgoing transactions. This takes ~8,000 RPC calls but runs incrementally — already-found blocks are cached in scan_state.
For each discovered Foundation tx block, eth_getBlockByNumber(..., true) fetches all transactions. The scanner finds the transfer matching from=Foundation, to=recipient, value≈100100 MON, input=0x.
For each approved address, the scanner binary-searches that address's nonce to find its first transaction, then scans forward to find the call to 0x1000 with selector 0xf145204c.
For each registered validator, the scanner compares eth_getTransactionCount(addr, latest) vs eth_getTransactionCount(addr, block_1h_ago). A delta > 60 transactions per hour indicates an active validator.
- Python 3.11+
- Ubuntu 24.04 (or similar)
sudoaccess for installing systemd services
cd /home/admin/monad-vdp
python3 -m venv venv
venv/bin/pip install -r requirements.txtCopy and edit .env:
cp .env.example .env # (or create from scratch)Available variables (all optional — defaults are sane):
RPC_URL=https://testnet-rpc.monad.xyz
FOUNDATION=0xFA735CcA8424e4EF30980653BF9015331d9929dB
CONTRACT=0x0000000000000000000000000000000000001000
DB_PATH=/home/admin/monad-vdp/vdp.db
WEB_PORT=8081
SCAN_INTERVAL=300
BOT_TOKEN= # Optional: Telegram bot token for alerts
FOUNDATION_MAX_NONCE=323 # Update as Foundation sends more txssudo cp monad-vdp-web.service /etc/systemd/system/
sudo cp monad-vdp-scanner.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now monad-vdp-web
sudo systemctl enable --now monad-vdp-scannerlocation /monad/vdp/ {
proxy_pass http://127.0.0.1:8081/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}journalctl -u monad-vdp-scanner -f
journalctl -u monad-vdp-web -fThe initial scan takes time. Binary-searching 323 nonces with 3 concurrent RPC calls and 0.3 s minimum delay between calls takes roughly:
- ~13 iterations per nonce × 323 nonces ÷ 3 concurrent ≈ 1,400 batches × 0.3 s ≈ ~7 minutes
Progress is saved after every 20 nonces, so a restart picks up where it left off.
The web dashboard shows a "Syncing X/323 txs" progress bar during this phase. All already-found data is displayed immediately.
These commands extend @MonadBeaconBot. To integrate, see the snippet at the bottom of bot_commands.py.
| Command | Description |
|---|---|
/watch <address> |
Subscribe to VDP approval alerts for an address |
/unwatch <address> |
Unsubscribe |
/vdp_status <address> |
Check approval + registration status |
/vdp_list |
Show last 10 VDP approvals |
When a watched address receives a VDP approval, the bot sends:
🎉 VDP Approved!
Address: 0x92936...b13B5
Amount: 100,100 MON received from Monad Foundation
Time: 2026-04-16 11:37 UTC
Next step: register your validator on testnet
Track progress: https://prooflines.org/monad/vdp/
| Endpoint | Description |
|---|---|
GET / |
Dashboard HTML |
GET /api/validators |
All validators with merged approval+registration+status |
GET /api/stats |
Aggregate stats + sync progress |
GET /api/validator/{address} |
Single address detail |
Five SQLite tables (WAL journal mode):
- approvals — Foundation approval txs indexed by recipient address
- registrations — Validator registrations indexed by address
- validator_status — Latest activity check results
- watch_subscriptions — Telegram user watch subscriptions
- scan_state — Scanner progress state (JSON blobs by key)
When the Foundation sends more approval txs, update FOUNDATION_MAX_NONCE in .env and restart the scanner. It will automatically scan only the new nonces.
# Check current Foundation nonce:
curl -s https://testnet-rpc.monad.xyz \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_getTransactionCount","params":["0xFA735CcA8424e4EF30980653BF9015331d9929dB","latest"],"id":1}' \
| python3 -c "import sys,json; print(int(json.load(sys.stdin)['result'],16))"MIT