Building Real-Time Ethereum Event Notifications with BLAZED.sh
Applications that react to blockchain events in real-time need low-latency infrastructure, especially when dealing with event filters that return large datasets (liquidation monitors, DEX aggregators, cross-chain bridges).
In this tutorial, we’ll build a webhook service that monitors Ethereum events and sends notifications to configured URLs, using BLAZED.sh’s IPC socket access for minimal latency.
Understanding BLAZED.sh’s Architecture
BLAZED.sh is optimized for Ethereum applications that require consistent low latency and predictable costs. While many providers offer WebSocket connections, BLAZED.sh provides exclusive IPC socket access (at /tmp/sockets/rpc_proxy.sock), eliminating network overhead entirely. This architectural choice ensures the lowest possible latency between your application and the Ethereum node.
For high-volume use cases like tracking hundreds of lending positions or monitoring swaps across dozens of pools, the data volume can get expensive fast on usage-based pricing. BLAZED.sh’s flat-rate pricing and IPC-based infrastructure keep costs predictable regardless of volume.
Event Signatures and Topics: The Building Blocks of Ethereum Events
Before diving into the code, it’s important to understand how Ethereum events work under the hood. Every smart contract event is identified by its signature, which is a 32-byte hash that uniquely identifies the event type. When a contract emits an event, this signature becomes the first topic in the event log.
For example, the standard ERC-20 Transfer event has the signature Transfer(address,address,uint256). When hashed using Keccak-256, this becomes 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. This hash is what we use to filter for specific events across the entire Ethereum blockchain.
Topics in Ethereum events serve as indexed parameters that allow for efficient filtering. The first topic is always the event signature, while topics 2-4 can contain indexed event parameters. For instance, in a Transfer event, topic 2 might be the sender address and topic 3 the recipient address. This indexing system allows us to create precise filters that only capture the events we care about, rather than processing every transaction on the network.
Setting Up the Project
Let’s start by creating a simple webhook service that demonstrates these concepts in action. We’ll focus on the core functionality without unnecessary complexity.
mkdir eth-webhook-service
cd eth-webhook-service
npm init -y
npm install ethers
Then add "type": "module" to your package.json to enable ES module imports.
The Core Webhook Service
Our webhook service will read configuration from a JSON file and establish event listeners for each configured webhook. Here’s the main implementation:
// webhook-service.js
import { ethers } from 'ethers';
import fs from 'fs';
class EthereumWebhookService {
constructor() {
this.provider = null;
this.webhooks = [];
this.subscriptions = new Map();
this.loadConfiguration();
this.connectToEthereum();
}
loadConfiguration() {
const configData = fs.readFileSync('./config.json', 'utf8');
const config = JSON.parse(configData);
this.webhooks = config.webhooks.filter(webhook => webhook.active);
console.log(`Loaded ${this.webhooks.length} active webhooks`);
}
async connectToEthereum() {
const ipcPath = process.env.ETHEREUM_IPC_PATH || '/tmp/sockets/rpc_proxy.sock';
this.provider = new ethers.IpcSocketProvider(ipcPath);
const network = await this.provider.getNetwork();
console.log(`Connected to ${network.name} (Chain ID: ${network.chainId})`);
this.startEventMonitoring();
}
startEventMonitoring() {
this.webhooks.forEach(webhook => {
const filter = { address: webhook.contractAddress, topics: [webhook.eventSignature] };
const eventListener = (log) => this.processEvent(webhook, log);
this.provider.on(filter, eventListener);
this.subscriptions.set(webhook.id, { filter, listener: eventListener });
console.log(`Monitoring ${webhook.name} events`);
});
}
async processEvent(webhook, log) {
const eventData = {
webhook: { id: webhook.id, name: webhook.name },
transaction: { hash: log.transactionHash, blockNumber: log.blockNumber, blockHash: log.blockHash, logIndex: log.logIndex },
event: { address: log.address, data: log.data, topics: log.topics },
timestamp: new Date().toISOString()
};
await this.deliverWebhook(webhook, eventData);
}
async deliverWebhook(webhook, eventData, attempt = 1) {
const maxAttempts = 3;
const response = await fetch(webhook.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData)
});
if (!response.ok && attempt < maxAttempts) {
setTimeout(() => this.deliverWebhook(webhook, eventData, attempt + 1), Math.pow(2, attempt - 1) * 1000);
}
}
start() {
console.log('Webhook service started');
process.on('SIGINT', () => {
this.subscriptions.forEach((subscription) => {
this.provider.off(subscription.filter, subscription.listener);
});
process.exit(0);
});
}
}
// Start the service
const service = new EthereumWebhookService();
service.start();
Configuration: Defining What Events to Monitor
The power of this webhook service lies in its configuration flexibility. Create a config.json file that defines exactly which events you want to monitor and where to send notifications:
{
"webhooks": [
{
"id": "usdc-transfers",
"name": "USDC Transfer Monitor",
"url": "https://your-app.com/api/webhooks/usdc-transfers",
"contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"eventSignature": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"active": true
},
{
"id": "compound-liquidations",
"name": "Compound Liquidation Events",
"url": "https://your-app.com/api/webhooks/liquidations",
"contractAddress": "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B",
"eventSignature": "0x298637f684da70674f26509b10f07ec2fbc77a335ab1e7d6215a4b2484d8bb52",
"active": true
}
]
}
Each webhook configuration specifies the contract address to monitor and the event signature to filter for. The event signature is crucial - it’s the Keccak-256 hash of the event’s function signature. For the standard ERC-20 Transfer event, this signature Transfer(address,address,uint256) hashes to the value shown above.
When monitoring DeFi protocols like Compound, liquidation events have their own unique signatures. The beauty of this approach is that you can monitor any event from any contract simply by knowing its signature and contract address.
Understanding Event Data Structure
When an event occurs, the webhook service receives a log object from ethers.js containing several important fields. The data field contains non-indexed event parameters encoded as hex, while the topics array contains indexed parameters. The first topic is always the event signature, and subsequent topics contain indexed parameters like addresses or token IDs.
For example, when monitoring USDC transfers, the log structure might look like this: the first topic contains the Transfer event signature, the second topic contains the sender address (padded to 32 bytes), the third topic contains the recipient address, and the data field contains the transfer amount. Understanding this structure allows you to decode and process events appropriately in your webhook handlers.
Deploying to BLAZED.sh
To deploy your webhook service, build and push your container image to a registry (Docker Hub, GitHub Container Registry, etc.):
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY webhook-service.js ./
CMD ["node", "webhook-service.js"]
Build and push your image:
docker build -t yourusername/eth-webhook:latest .
docker push yourusername/eth-webhook:latest
Then deploy the container on BLAZED.sh with the socket mounted at /tmp/sockets/rpc_proxy.sock. The platform provides low-latency access to fully synchronized Ethereum nodes with predictable flat-rate pricing.
Testing Your Webhook Service
To test your webhook service locally, you can create a simple HTTP server that logs incoming webhooks:
// test-receiver.js
import http from 'http';
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const webhook = JSON.parse(body);
console.log('Received webhook:', webhook.webhook.name, webhook.transaction.hash);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: true }));
});
}
});
server.listen(8080, () => console.log('Test receiver on port 8080'));
Update your config.json to point to http://localhost:8080 and run both services to see events flowing in real-time.
Advanced Event Filtering and Multiple Topics
While our basic example monitors all events of a specific type from a contract, you can create more sophisticated filters using multiple topics. For instance, if you only wanted to monitor USDC transfers to a specific address, you could modify the event listener setup:
const filter = {
address: webhook.contractAddress,
topics: [webhook.eventSignature, null, ethers.zeroPadValue('0x742d35Cc6634C0532925a3b8D12C0b5cb5aAa101', 32)]
};
This level of precision ensures your webhook service only processes the events that matter to your application, reducing noise and improving performance.
Cost Considerations
Event monitoring costs scale with volume. A DEX aggregator tracking all Uniswap V3 pools might process thousands of swap events per block during volatile periods. On usage-based infrastructure, that adds up fast.
BLAZED.sh uses flat-rate pricing; 100 events or 100,000 events per day cost the same. This works well for:
- Trading bots monitoring multiple DEX pools
- Liquidation services tracking positions across lending protocols
- NFT tools monitoring collections for rare traits
- Cross-chain bridges processing high volumes of transfer events
Conclusion
The webhook service above is a working starting point for real-time Ethereum event monitoring. The key pieces are ethers.js event filters, a config-driven webhook setup, and retry logic for delivery. Pair it with BLAZED.sh’s IPC access and flat-rate pricing for predictable costs at any volume.