This tutorial teaches you how to monitor on-chain events and transactions on Monad. You’ll use WebSocket subscriptions (eth_subscribe) for real-time streaming, with HTTP polling via eth_getLogs as an alternative pattern for environments where WebSocket isn’t an option.
Get your own node endpoint todayStart for free and get your app to production levels immediately. No credit card required.You can sign up with your GitHub, X, Google, or Microsoft account.
TLDR:
- Stream real-time events with
eth_subscribe("logs", {...}) over WebSocket
- Stream new blocks with
eth_subscribe("newHeads")
- Alternative: query logs over HTTP with
eth_getLogs
- Monitor ERC-20 Transfer events on WMON (Wrapped MON)
- Handle Monad’s high-throughput blocks efficiently
- Both JavaScript and Python implementations
Prerequisites
- Chainstack account with a Monad node endpoint
- Node.js v16+ or Python 3.8+
- Basic understanding of Ethereum events and logs
Overview
Event monitoring on Monad differs from Ethereum in a few ways:
- WebSocket subscriptions —
eth_subscribe("newHeads") and eth_subscribe("logs", {...}) are supported for real-time streaming. eth_subscribe("newPendingTransactions") and eth_subscribe("syncing") are not supported and return -32602 Invalid params.
- 1-second blocks — events appear roughly every second
- High transaction volume — blocks contain many transactions, so when using
eth_getLogs, query small block ranges
- Immediate finality — no need to wait for confirmations or handle reorgs
This tutorial shows you how to build efficient monitoring tools that work with Monad’s architecture.
Understanding Monad’s event system
Events (logs) on Monad work the same as Ethereum:
- Smart contracts emit events during execution
- Events are stored in transaction receipts
- You stream events in real time with
eth_subscribe("logs", {...}) over WebSocket, or query historical events with eth_getLogs over HTTP
Pick the pattern that fits your use case:
- Real-time streaming — use
eth_subscribe over WebSocket. Lower latency, no polling loop, no missed blocks
- Historical queries or HTTP-only environments — use
eth_getLogs. Use small block ranges (1–10 blocks) per query, and process logs immediately since they’re final
Stream events in real time with eth_subscribe
eth_subscribe("logs", {...}) streams matching events to you over a WebSocket connection as soon as a block containing them lands. No polling, no missed blocks, no extra request budget burned on idle intervals.
JavaScript — subscribe to WMON Transfers
const { ethers } = require("ethers");
const CHAINSTACK_WSS_ENDPOINT = "YOUR_CHAINSTACK_WSS_ENDPOINT";
const WMON_ADDRESS = "0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701";
const TRANSFER_TOPIC = ethers.id("Transfer(address,address,uint256)");
const provider = new ethers.WebSocketProvider(CHAINSTACK_WSS_ENDPOINT);
function decodeTransfer(log) {
const from = "0x" + log.topics[1].slice(26);
const to = "0x" + log.topics[2].slice(26);
const value = BigInt(log.data);
return {
from,
to,
value: ethers.formatEther(value),
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
};
}
async function subscribe() {
console.log(`Subscribing to WMON Transfer events at ${WMON_ADDRESS}`);
const filter = {
address: WMON_ADDRESS,
topics: [TRANSFER_TOPIC],
};
provider.on(filter, (log) => {
const t = decodeTransfer(log);
console.log(`Block ${t.blockNumber}: ${t.value} WMON from ${t.from} to ${t.to}`);
console.log(` Tx: ${t.transactionHash}`);
});
}
subscribe();
Run:
Under the hood, provider.on(filter, handler) sends a raw eth_subscribe request:
{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["logs",{"address":"0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701","topics":["0xddf252ad..."]}]}
Python — subscribe to WMON Transfers
import asyncio
import json
import websockets
CHAINSTACK_WSS_ENDPOINT = "YOUR_CHAINSTACK_WSS_ENDPOINT"
WMON_ADDRESS = "0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701"
# keccak256("Transfer(address,address,uint256)")
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
async def subscribe():
async with websockets.connect(CHAINSTACK_WSS_ENDPOINT) as ws:
await ws.send(json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "eth_subscribe",
"params": ["logs", {
"address": WMON_ADDRESS,
"topics": [TRANSFER_TOPIC],
}],
}))
sub = json.loads(await ws.recv())
print(f"Subscribed: {sub['result']}")
async for raw in ws:
msg = json.loads(raw)
if msg.get("method") != "eth_subscription":
continue
log = msg["params"]["result"]
from_addr = "0x" + log["topics"][1][26:]
to_addr = "0x" + log["topics"][2][26:]
value_wei = int(log["data"], 16)
value = value_wei / 10**18
print(f"Block {int(log['blockNumber'], 16)}: {value} WMON "
f"from {from_addr} to {to_addr}")
print(f" Tx: {log['transactionHash']}")
asyncio.run(subscribe())
Run:
pip install websockets
python subscribe_wmon.py
Use eth_subscribe("newHeads") to react to every new block as it’s produced:
const { ethers } = require("ethers");
const CHAINSTACK_WSS_ENDPOINT = "YOUR_CHAINSTACK_WSS_ENDPOINT";
const provider = new ethers.WebSocketProvider(CHAINSTACK_WSS_ENDPOINT);
provider.on("block", async (blockNumber) => {
const block = await provider.getBlock(blockNumber);
const utilization = ((Number(block.gasUsed) / Number(block.gasLimit)) * 100).toFixed(2);
console.log(`Block ${blockNumber}: ${block.transactions.length} txs, gas ${utilization}%`);
});
Alternative: poll with eth_getLogs
If you can’t use WebSocket — for example, in a serverless function, a browser environment without persistent sockets, or a constrained runtime — poll eth_getLogs over HTTP instead.
JavaScript polling implementation
const { ethers } = require("ethers");
const CHAINSTACK_ENDPOINT = "YOUR_CHAINSTACK_MONAD_ENDPOINT";
const WMON_ADDRESS = "0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701";
// Transfer event signature: keccak256("Transfer(address,address,uint256)")
const TRANSFER_TOPIC = ethers.id("Transfer(address,address,uint256)");
const provider = new ethers.JsonRpcProvider(CHAINSTACK_ENDPOINT);
async function getTransferEvents(fromBlock, toBlock) {
const filter = {
address: WMON_ADDRESS,
topics: [TRANSFER_TOPIC],
fromBlock: fromBlock,
toBlock: toBlock,
};
const logs = await provider.getLogs(filter);
return logs;
}
function decodeTransferEvent(log) {
// Transfer(address indexed from, address indexed to, uint256 value)
const from = "0x" + log.topics[1].slice(26);
const to = "0x" + log.topics[2].slice(26);
const value = BigInt(log.data);
return {
from,
to,
value: ethers.formatEther(value),
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
logIndex: log.index,
};
}
async function monitorTransfers() {
let lastBlock = await provider.getBlockNumber();
console.log(`Starting WMON Transfer monitor at block ${lastBlock}`);
console.log(`Contract: ${WMON_ADDRESS}\n`);
// Poll every second
setInterval(async () => {
try {
const currentBlock = await provider.getBlockNumber();
if (currentBlock > lastBlock) {
// Query new blocks
const fromBlock = lastBlock + 1;
const toBlock = currentBlock;
console.log(`Checking blocks ${fromBlock} to ${toBlock}...`);
const logs = await getTransferEvents(fromBlock, toBlock);
if (logs.length > 0) {
console.log(`Found ${logs.length} Transfer events:\n`);
for (const log of logs) {
const transfer = decodeTransferEvent(log);
console.log(`Block ${transfer.blockNumber}:`);
console.log(` From: ${transfer.from}`);
console.log(` To: ${transfer.to}`);
console.log(` Amount: ${transfer.value} WMON`);
console.log(` Tx: ${transfer.transactionHash}\n`);
}
}
lastBlock = currentBlock;
}
} catch (error) {
console.error("Error:", error.message);
}
}, 1000);
}
monitorTransfers();
Run:
Python polling implementation
from web3 import Web3
import time
CHAINSTACK_ENDPOINT = "YOUR_CHAINSTACK_MONAD_ENDPOINT"
WMON_ADDRESS = "0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701"
# Transfer event signature
TRANSFER_TOPIC = Web3.keccak(text="Transfer(address,address,uint256)").hex()
web3 = Web3(Web3.HTTPProvider(CHAINSTACK_ENDPOINT))
print(f"Connected: {web3.is_connected()}")
def get_transfer_events(from_block, to_block):
"""Fetch Transfer events in a block range."""
filter_params = {
'address': WMON_ADDRESS,
'topics': [TRANSFER_TOPIC],
'fromBlock': from_block,
'toBlock': to_block
}
logs = web3.eth.get_logs(filter_params)
return logs
def decode_transfer_event(log):
"""Decode a Transfer event log."""
from_address = '0x' + log['topics'][1].hex()[26:]
to_address = '0x' + log['topics'][2].hex()[26:]
value = int(log['data'].hex(), 16)
return {
'from': Web3.to_checksum_address(from_address),
'to': Web3.to_checksum_address(to_address),
'value': web3.from_wei(value, 'ether'),
'block_number': log['blockNumber'],
'transaction_hash': log['transactionHash'].hex(),
'log_index': log['logIndex']
}
def monitor_transfers():
"""Monitor WMON Transfer events continuously."""
last_block = web3.eth.block_number
print(f"Starting WMON Transfer monitor at block {last_block}")
print(f"Contract: {WMON_ADDRESS}\n")
while True:
try:
current_block = web3.eth.block_number
if current_block > last_block:
from_block = last_block + 1
to_block = current_block
print(f"Checking blocks {from_block} to {to_block}...")
logs = get_transfer_events(from_block, to_block)
if logs:
print(f"Found {len(logs)} Transfer events:\n")
for log in logs:
transfer = decode_transfer_event(log)
print(f"Block {transfer['block_number']}:")
print(f" From: {transfer['from']}")
print(f" To: {transfer['to']}")
print(f" Amount: {transfer['value']} WMON")
print(f" Tx: {transfer['transaction_hash']}\n")
last_block = current_block
# Poll every second
time.sleep(1)
except Exception as e:
print(f"Error: {e}")
time.sleep(1)
if __name__ == "__main__":
monitor_transfers()
Run:
Build a block monitor with polling
Monitor all transactions in new blocks over HTTP. For a WebSocket equivalent, see the eth_subscribe("newHeads") example above.
JavaScript block monitor
const { ethers } = require("ethers");
const CHAINSTACK_ENDPOINT = "YOUR_CHAINSTACK_MONAD_ENDPOINT";
const provider = new ethers.JsonRpcProvider(CHAINSTACK_ENDPOINT);
async function processBlock(blockNumber) {
const block = await provider.getBlock(blockNumber, true); // true = include transactions
const timestamp = new Date(block.timestamp * 1000).toISOString();
const gasUsed = block.gasUsed.toString();
const gasLimit = block.gasLimit.toString();
const utilization = ((Number(block.gasUsed) / Number(block.gasLimit)) * 100).toFixed(2);
console.log(`\n=== Block ${blockNumber} ===`);
console.log(`Timestamp: ${timestamp}`);
console.log(`Transactions: ${block.transactions.length}`);
console.log(`Gas used: ${gasUsed} / ${gasLimit} (${utilization}%)`);
console.log(`Miner: ${block.miner}`);
// Process individual transactions if needed
if (block.prefetchedTransactions && block.prefetchedTransactions.length > 0) {
// Show first 5 transactions
const txs = block.prefetchedTransactions.slice(0, 5);
console.log(`\nFirst ${txs.length} transactions:`);
for (const tx of txs) {
const value = ethers.formatEther(tx.value);
console.log(` ${tx.hash.slice(0, 18)}... | ${tx.from.slice(0, 10)}... -> ${tx.to?.slice(0, 10) || 'Contract creation'}... | ${value} MON`);
}
if (block.prefetchedTransactions.length > 5) {
console.log(` ... and ${block.prefetchedTransactions.length - 5} more`);
}
}
}
async function monitorBlocks() {
let lastBlock = await provider.getBlockNumber();
console.log(`Starting block monitor at block ${lastBlock}`);
setInterval(async () => {
try {
const currentBlock = await provider.getBlockNumber();
if (currentBlock > lastBlock) {
for (let i = lastBlock + 1; i <= currentBlock; i++) {
await processBlock(i);
}
lastBlock = currentBlock;
}
} catch (error) {
console.error("Error:", error.message);
}
}, 1000);
}
monitorBlocks();
Python block monitor
from web3 import Web3
import time
from datetime import datetime
CHAINSTACK_ENDPOINT = "YOUR_CHAINSTACK_MONAD_ENDPOINT"
web3 = Web3(Web3.HTTPProvider(CHAINSTACK_ENDPOINT))
def process_block(block_number):
"""Process and display block information."""
block = web3.eth.get_block(block_number, full_transactions=True)
timestamp = datetime.fromtimestamp(block.timestamp).isoformat()
gas_used = block.gasUsed
gas_limit = block.gasLimit
utilization = (gas_used / gas_limit) * 100
print(f"\n=== Block {block_number} ===")
print(f"Timestamp: {timestamp}")
print(f"Transactions: {len(block.transactions)}")
print(f"Gas used: {gas_used:,} / {gas_limit:,} ({utilization:.2f}%)")
print(f"Miner: {block.miner}")
# Show first 5 transactions
if block.transactions:
txs = block.transactions[:5]
print(f"\nFirst {len(txs)} transactions:")
for tx in txs:
value = web3.from_wei(tx.value, 'ether')
to_addr = tx.to[:10] + '...' if tx.to else 'Contract creation'
print(f" {tx.hash.hex()[:18]}... | {tx['from'][:10]}... -> {to_addr} | {value:.4f} MON")
if len(block.transactions) > 5:
print(f" ... and {len(block.transactions) - 5} more")
def monitor_blocks():
"""Monitor new blocks continuously."""
last_block = web3.eth.block_number
print(f"Starting block monitor at block {last_block}")
while True:
try:
current_block = web3.eth.block_number
if current_block > last_block:
for block_num in range(last_block + 1, current_block + 1):
process_block(block_num)
last_block = current_block
time.sleep(1)
except Exception as e:
print(f"Error: {e}")
time.sleep(1)
if __name__ == "__main__":
monitor_blocks()
Monitor custom contract events
Track events from any contract by defining the event signature:
const { ethers } = require("ethers");
const CHAINSTACK_ENDPOINT = "YOUR_CHAINSTACK_MONAD_ENDPOINT";
const provider = new ethers.JsonRpcProvider(CHAINSTACK_ENDPOINT);
// Example: Monitor any ERC-20 Approval events
const APPROVAL_TOPIC = ethers.id("Approval(address,address,uint256)");
// Or monitor a specific contract
const CONTRACT_ADDRESS = "YOUR_CONTRACT_ADDRESS";
const CUSTOM_EVENT_TOPIC = ethers.id("YourEvent(address,uint256)");
async function monitorEvents(address, topics, eventName) {
let lastBlock = await provider.getBlockNumber();
console.log(`Monitoring ${eventName} events from block ${lastBlock}`);
setInterval(async () => {
try {
const currentBlock = await provider.getBlockNumber();
if (currentBlock > lastBlock) {
const filter = {
address: address, // null for all contracts
topics: topics,
fromBlock: lastBlock + 1,
toBlock: currentBlock,
};
const logs = await provider.getLogs(filter);
if (logs.length > 0) {
console.log(`\nFound ${logs.length} ${eventName} events:`);
for (const log of logs) {
console.log(` Block ${log.blockNumber}: ${log.transactionHash}`);
}
}
lastBlock = currentBlock;
}
} catch (error) {
console.error("Error:", error.message);
}
}, 1000);
}
// Monitor all ERC-20 Approvals on Monad
monitorEvents(null, [APPROVAL_TOPIC], "Approval");
Filter events by address
Monitor transfers to or from a specific address:
const { ethers } = require("ethers");
const CHAINSTACK_ENDPOINT = "YOUR_CHAINSTACK_MONAD_ENDPOINT";
const WMON_ADDRESS = "0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701";
const WATCH_ADDRESS = "YOUR_ADDRESS_TO_WATCH";
const TRANSFER_TOPIC = ethers.id("Transfer(address,address,uint256)");
const provider = new ethers.JsonRpcProvider(CHAINSTACK_ENDPOINT);
async function monitorAddressTransfers(address) {
let lastBlock = await provider.getBlockNumber();
// Pad address to 32 bytes for topic matching
const paddedAddress = ethers.zeroPadValue(address, 32);
console.log(`Monitoring WMON transfers for ${address}`);
console.log(`Starting at block ${lastBlock}\n`);
setInterval(async () => {
try {
const currentBlock = await provider.getBlockNumber();
if (currentBlock > lastBlock) {
// Incoming transfers (address is 'to')
const incomingFilter = {
address: WMON_ADDRESS,
topics: [TRANSFER_TOPIC, null, paddedAddress],
fromBlock: lastBlock + 1,
toBlock: currentBlock,
};
// Outgoing transfers (address is 'from')
const outgoingFilter = {
address: WMON_ADDRESS,
topics: [TRANSFER_TOPIC, paddedAddress, null],
fromBlock: lastBlock + 1,
toBlock: currentBlock,
};
const [incomingLogs, outgoingLogs] = await Promise.all([
provider.getLogs(incomingFilter),
provider.getLogs(outgoingFilter),
]);
for (const log of incomingLogs) {
const from = "0x" + log.topics[1].slice(26);
const value = ethers.formatEther(BigInt(log.data));
console.log(`📥 INCOMING: ${value} WMON from ${from}`);
console.log(` Tx: ${log.transactionHash}\n`);
}
for (const log of outgoingLogs) {
const to = "0x" + log.topics[2].slice(26);
const value = ethers.formatEther(BigInt(log.data));
console.log(`📤 OUTGOING: ${value} WMON to ${to}`);
console.log(` Tx: ${log.transactionHash}\n`);
}
lastBlock = currentBlock;
}
} catch (error) {
console.error("Error:", error.message);
}
}, 1000);
}
monitorAddressTransfers(WATCH_ADDRESS);
Best practices for Monad
Optimize your monitoring for Monad’s characteristics:
-
Prefer
eth_subscribe over polling — WebSocket subscriptions are lower latency and don’t burn request budget on idle intervals. Reach for eth_getLogs polling only when WebSocket isn’t an option.
-
Use small block ranges when polling — query 1–10 blocks at a time. Monad blocks can contain thousands of transactions.
-
Match polling cadence to block time — Monad produces blocks every ~1 second. Polling faster wastes requests; slower misses events.
-
Handle high volume — be prepared for blocks with many events. Process asynchronously if needed.
-
No reorg handling needed — Monad has instant finality. Once you see an event, it’s permanent.
-
Batch your queries — if monitoring multiple contracts, combine filters where possible.
eth_getLogs limitations:
- Most nodes limit the block range per query (typically 1,000–10,000 blocks)
- For historical data, paginate through block ranges
- For real-time monitoring, stick to recent blocks only — or use
eth_subscribe("logs", {...}) instead
Monad-specific notes
Key differences from Ethereum monitoring:
eth_subscribe subset — newHeads and logs are supported. eth_subscribe("newPendingTransactions") and eth_subscribe("syncing") are not supported on Monad and return -32602 Invalid params.
- 1-second blocks — events appear faster than on Ethereum. Your monitor needs to keep up.
- No pending transactions — you can’t monitor the mempool. Only confirmed transactions are visible.
- Immediate finality — no need to wait for confirmations. Events are final when you see them.
- High throughput — expect more events per block than on Ethereum. Design accordingly.
Complete monitoring script
A production-ready monitor combining all techniques:
const { ethers } = require("ethers");
const CHAINSTACK_ENDPOINT = "YOUR_CHAINSTACK_MONAD_ENDPOINT";
const provider = new ethers.JsonRpcProvider(CHAINSTACK_ENDPOINT);
class MonadEventMonitor {
constructor(config) {
this.lastBlock = 0;
this.config = config;
this.isRunning = false;
}
async start() {
this.lastBlock = await provider.getBlockNumber();
this.isRunning = true;
console.log(`Monitor started at block ${this.lastBlock}`);
this.poll();
}
stop() {
this.isRunning = false;
console.log("Monitor stopped");
}
async poll() {
while (this.isRunning) {
try {
const currentBlock = await provider.getBlockNumber();
if (currentBlock > this.lastBlock) {
await this.processNewBlocks(this.lastBlock + 1, currentBlock);
this.lastBlock = currentBlock;
}
await this.sleep(1000);
} catch (error) {
console.error("Poll error:", error.message);
await this.sleep(1000);
}
}
}
async processNewBlocks(fromBlock, toBlock) {
for (const filter of this.config.filters) {
const filterWithBlocks = {
...filter.params,
fromBlock,
toBlock,
};
const logs = await provider.getLogs(filterWithBlocks);
if (logs.length > 0) {
filter.handler(logs);
}
}
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Example usage
const WMON = "0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701";
const TRANSFER = ethers.id("Transfer(address,address,uint256)");
const monitor = new MonadEventMonitor({
filters: [
{
name: "WMON Transfers",
params: {
address: WMON,
topics: [TRANSFER],
},
handler: (logs) => {
console.log(`\n📊 ${logs.length} WMON transfers detected`);
for (const log of logs.slice(0, 3)) {
const value = ethers.formatEther(BigInt(log.data));
console.log(` ${value} WMON in block ${log.blockNumber}`);
}
if (logs.length > 3) {
console.log(` ... and ${logs.length - 3} more`);
}
},
},
],
});
monitor.start();
// Stop after 60 seconds (for demo)
// setTimeout(() => monitor.stop(), 60000);
Next steps
Now that you can monitor events on Monad, you can:
- Build real-time dashboards for DEX activity
- Create alerting systems for large transfers
- Track NFT mints and sales
- Monitor your own smart contract events
- Build analytics pipelines for on-chain data
Last modified on April 13, 2026