Subscribe to smart contract events on Stable with ethers.js watchContractEvent and build a live event stream.
Indexing turns on-chain events into data your application can react to: balance updates, transaction history, UI notifications. This guide shows how to subscribe to events from a deployed Stable contract using ethers.js and how to backfill historical events so you don’t miss any emitted while your service was offline.
Use a WebSocket provider so you receive events as soon as validators finalize each block. WebSocket avoids polling overhead and keeps notification latency close to block time (~0.7 seconds on Stable).
When a service starts, you usually need to catch up on events emitted while it was offline. Use queryFilter with a block range.
// backfill.tsimport { ethers } from "ethers";import { STABLE_TESTNET_RPC, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config";const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);const latest = await provider.getBlockNumber();const fromBlock = Math.max(0, latest - 10_000); // last ~10k blocksconst events = await contract.queryFilter( contract.filters.NumberUpdated(), fromBlock, latest);for (const event of events) { console.log( `[block ${event.blockNumber}]`, event.args.caller, "set number to", event.args.newValue.toString() );}console.log(`Backfilled ${events.length} events from block ${fromBlock} to ${latest}`);
npx tsx backfill.ts
[block 1282351] 0x1234...abcd set number to 10[block 1283092] 0xef01...2345 set number to 25[block 1284371] 0x1234...abcd set number to 42Backfilled 3 events from block 1282351 to 1284371
Wide block ranges (millions of blocks) can exceed RPC rate limits and time out. For production indexers, paginate by 10k-block windows or use Stablescan’s Etherscan-compatible API for indexed historical queries.
Events with indexed parameters (like caller above) can be filtered server-side. Pass the filter value instead of reading every event and filtering in your app.
// watchUser.tsimport { ethers } from "ethers";import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config";const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS);const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);const userAddress = "0x1234...abcd";const filter = contract.filters.NumberUpdated(userAddress);contract.on(filter, (caller, oldValue, newValue, event) => { console.log(`${caller} set number to ${newValue.toString()}`);});console.log(`Watching NumberUpdated for ${userAddress}...`);
npx tsx watchUser.ts
Watching NumberUpdated for 0x1234...abcd...0x1234...abcd set number to 42