Monitoring On-Chain Metrics of Your Project
Your smart contract is deployed. Does the team know what's happening with it in real-time? How many unique users interacted in the last 24 hours? Is transaction volume growing abnormally right now? Are there patterns indicating exploitation attempts? On-chain monitoring answers these questions before problems appear, not after.
What to Monitor and Why
I divide metrics into three categories by purpose:
Operations — for DevOps and team:
- Call frequency of key contract functions
- Gas consumption by method (abnormal growth = expensive operations or attack)
- Failed transaction count (growth in failed/total ratio = possible attack or bug)
- TVL (Total Value Locked) if contract holds assets
Business — for product:
- DAU/MAU on-chain (unique addresses per period)
- Retention — addresses returning after N days
- Volume in USD through specific functions
- Top-N addresses by activity (whale monitoring)
Security — for Security team:
- Large withdrawals (> threshold) in short period
- Unusual call patterns (flash loan + contract in one block)
- Changes in ownership/admin role
- Calls from new addresses with large balance
Data Sources
Event logs — primary source. Well-designed contract emits event for each significant action. If missing — add them (if upgradeable) or use traces.
Trace calls — internal calls between contracts. Need debug_traceTransaction or trace_transaction. Used for tracking funds through multiple contracts in one transaction.
Storage slots — direct state reading. eth_getStorageAt(address, slot, blockNumber). Useful for metrics not emitted as events: current TVL, pool sizes.
Dune Analytics / Flipside — SQL queries to indexed blockchain data. Fast start for analytics, but vendor lock-in and delayed data (not real-time).
Monitoring Architecture
┌─────────────────┐
│ Blockchain RPC │
│ (Alchemy/own) │
└────────┬────────┘
│ eth_getLogs / WebSocket
┌────────────▼──────────────┐
│ Event Collector │
│ (contract subscription) │
└────────────┬──────────────┘
│
┌────────────▼──────────────┐
│ Metrics Processor │
│ ABI decoding, │
│ aggregation, enrichment │
└──────┬────────────┬────────┘
│ │
┌────────────▼──┐ ┌──────▼──────────────┐
│ TimescaleDB │ │ Prometheus/VictoriaDB│
│ (history) │ │ (real-time metrics) │
└───────────────┘ └──────────────────────┘
│
┌─────────▼──────────┐
│ Grafana Dashboard │
│ + Alertmanager │
└────────────────────┘
Event Collector Implementation
import { createPublicClient, webSocket, parseAbiItem, decodeEventLog } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: webSocket('wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
});
const CONTRACT_ABI = [
parseAbiItem('event Deposit(address indexed user, uint256 amount)'),
parseAbiItem('event Withdraw(address indexed user, uint256 amount)'),
parseAbiItem('event Swap(address indexed user, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut)'),
];
// WebSocket subscription to contract events
const unwatch = client.watchContractEvent({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
onLogs: async (logs) => {
for (const log of logs) {
await processEvent(log);
}
},
onError: (error) => {
console.error('WS error, reconnecting...', error);
// viem auto-reconnects
},
});
async function processEvent(log: any) {
const decoded = decodeEventLog({ abi: CONTRACT_ABI, ...log });
// Write to TimescaleDB
await db.query(`
INSERT INTO contract_events
(time, block_number, tx_hash, event_name, user_address, amount_usd)
VALUES (NOW(), $1, $2, $3, $4, $5)
`, [log.blockNumber, log.transactionHash, decoded.eventName,
decoded.args.user, await convertToUSD(decoded.args.amount)]);
// Increment Prometheus counter
eventCounter.labels(decoded.eventName).inc();
}
Prometheus Metrics and Alerts
import { Counter, Gauge, Histogram, Registry } from 'prom-client';
const registry = new Registry();
const eventCounter = new Counter({
name: 'contract_events_total',
help: 'Total contract events by type',
labelNames: ['event_name'],
registers: [registry],
});
const tvlGauge = new Gauge({
name: 'contract_tvl_usd',
help: 'Total Value Locked in USD',
registers: [registry],
});
const largeWithdrawalCounter = new Counter({
name: 'contract_large_withdrawals_total',
help: 'Withdrawals above threshold',
labelNames: ['threshold_category'],
registers: [registry],
});
// HTTP endpoint for Prometheus scraping
app.get('/metrics', async (req, res) => {
res.set('Content-Type', registry.contentType);
res.send(await registry.metrics());
});
Alertmanager rules:
groups:
- name: contract_security
rules:
# Large withdrawal — immediate alert
- alert: LargeWithdrawal
expr: rate(contract_large_withdrawals_total[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Large withdrawal detected from contract"
# Abnormal failed transaction growth
- alert: HighFailureRate
expr: |
rate(contract_failed_txns_total[10m]) /
rate(contract_total_txns_total[10m]) > 0.1
for: 5m
annotations:
summary: "More than 10% of transactions failing"
# TVL drops fast
- alert: TVLDrop
expr: |
(contract_tvl_usd - contract_tvl_usd offset 1h) /
contract_tvl_usd offset 1h < -0.2
for: 2m
annotations:
summary: "TVL dropped by more than 20% in 1 hour"
Whale and Anomaly Detector
import asyncio
from web3 import AsyncWeb3
WHALE_THRESHOLD_USD = 100_000
async def monitor_large_transactions(contract, event_name: str):
async for event in contract.events[event_name].get_logs(fromBlock='latest'):
amount_usd = await get_usd_value(event.args.amount, event.args.token)
if amount_usd > WHALE_THRESHOLD_USD:
await send_telegram_alert(
f"🐋 Whale {event_name}: ${amount_usd:,.0f}\n"
f"Address: {event.args.user}\n"
f"Tx: https://etherscan.io/tx/{event.transactionHash.hex()}"
)
# Check flash loan pattern: deposit + withdraw in one block
if await is_flash_loan_pattern(event):
await send_telegram_alert(
f"⚠️ Flash loan pattern detected in block {event.blockNumber}",
level='WARNING'
)
Grafana Dashboard
Key panels for DeFi protocol:
Overview: TVL (gauge + time series), 24h Volume, DAU, Total Users (cumulative)
Activity: Events per minute (time series, by type), Gas used per block, Failed tx ratio
Security: Large transactions (table with recent whale txns), New whale addresses (first txn in contract + large amount), Flash loan detection events
Economics: Fee revenue over time, Token price correlation with activity
Dashboard as JSON — versioned in git with contract code.
Historical Data and Retrospective Analysis
Real-time monitoring catches anomalies now. For retrospective analysis need history:
-- Daily active users
SELECT
date_trunc('day', time) AS day,
COUNT(DISTINCT user_address) AS dau,
SUM(amount_usd) AS volume_usd
FROM contract_events
WHERE event_name IN ('Deposit', 'Swap')
GROUP BY 1
ORDER BY 1 DESC;
-- Retention: users returning after 7 days
WITH first_use AS (
SELECT user_address, MIN(time) AS first_time
FROM contract_events GROUP BY 1
),
return_use AS (
SELECT DISTINCT e.user_address
FROM contract_events e
JOIN first_use f ON e.user_address = f.user_address
WHERE e.time > f.first_time + INTERVAL '7 days'
AND e.time < f.first_time + INTERVAL '14 days'
)
SELECT
COUNT(r.user_address)::float / COUNT(f.user_address) AS week1_retention
FROM first_use f
LEFT JOIN return_use r ON f.user_address = r.user_address;
Deployment Process
Day 1: event collector setup, RPC connection, raw event writing to TimescaleDB. Test on several blocks with real transactions.
Day 2: Prometheus metrics, first Grafana dashboard, setup basic alerts (TVL drop, high failure rate).
Day 3: whale detector, security alerts, test alerts via transaction simulation, team runbook.
Total 1-3 days depending on contract complexity and number of metrics monitored.







