Designing an Event-Driven Web3 Bot: Fastify, BullMQ, and MetaMask
"Architecting a robust, non-custodial NFT sniper bot with real-time WebSocket dashboard and distributed job queues."
Trading bots on low-latency blockchains need to process real-time market streams, evaluate trade strategies, and execute blockchain transactions with sub-second response times. Building such a system requires a modular, event-driven architecture that separates fast market monitoring from heavy write database actions and wallet execution.
Here is how we designed a non-custodial NFT Sniper Bot dashboard running on the Monad network.
The Architecture Layout
Our system uses an event-driven microservices setup:
[Market Websocket API]
│
▼ (Real-time feed)
[Fastify API / Webhook] ──> [Redis/BullMQ Queue] ──> [Worker Pool]
│
▼ (Prepare Tx)
[Client Browser] <─── WebSocket ─── [Realtime UI] <─── [MetaMask Sign]
- Market Feed: A listener service polls or connects to marketplace WebSocket feeds (like OpenSea or Magic Eden) and immediately dispatches new listings to a high-speed message queue.
- Queueing (BullMQ): Because database updates or transaction compilation can delay event handling, we offload processing to BullMQ backed by Redis.
- Transaction Preparation: The worker processes listings, checks active user filters, and compiles an unsigned EVM transaction.
- Non-Custodial Signing: To keep keys secure, we push the transaction to the user’s browser via WebSockets. The user signs and submits the transaction locally using MetaMask.
Implementing the Transaction Lifecycle
Below is a simplified Fastify route demonstrating how the backend handles transaction registration and monitors status updates over EVM RPC:
import Fastify from 'fastify';
import Queue from 'bull';
import { ethers } from 'ethers';
const app = Fastify();
const tradeQueue = new Queue('trades', 'redis://127.0.0.1:6379');
const provider = new ethers.JsonRpcProvider("https://testnet-rpc.monad.xyz");
// 1. Endpoint to register a new snipe target candidate
app.post('/api/snipe', async (request, reply) => {
const { listingId, price, targetToken } = request.body as any;
// Add job to processing queue immediately to prevent endpoint blocking
const job = await tradeQueue.add({
listingId,
price,
targetToken,
status: 'PREPARING'
});
return { jobId: job.id, status: 'QUEUED' };
});
// 2. Background RPC scanner checks if a sent transaction has been mined
async function checkTransactionStatus(txHash: string) {
try {
const receipt = await provider.getTransactionReceipt(txHash);
if (receipt) {
const status = receipt.status === 1 ? 'SUCCESS' : 'FAILED';
console.log(`Transaction ${txHash} mined with status: ${status}`);
// Trigger database and socket updates here
}
} catch (error) {
console.error("RPC Error:", error);
}
}
Scaling the Queue
Using BullMQ guarantees that if the transaction execution node experiences high latency, listing events are buffered safely in Redis.
| Component | Technology | Role |
|---|---|---|
| API Layer | Fastify | Handles incoming listing events and client requests with minimal overhead. |
| Broker | Redis (BullMQ) | Handles task routing, strategy matching, and retry operations. |
| State Store | PostgreSQL | Persists user credentials, historical trades, and performance metrics. |
| EVM Client | MetaMask (Frontend) | Secures private keys locally, executing non-custodial transactions. |