Skip to main content

How to Filter Mempool Transactions on Ethereum

Created on
Updated on
Nov 26, 2024

7 min read

Overview

Before transactions on Ethereum are processed, they pass through a buffer zone called the Mempool. The Mempool is a valuable source of information for users who want to detect specific transactions as they occur in real-time. This guide will teach you how to retrieve and filter Mempool transactions on Ethereum using a WebSocket connection and JavaScript. Specifically, we will be filtering through the Mempool to find incoming transactions that want to interact with the Uniswap V3 protocol.

What You Will Need

  • Node.js & npm
  • An Ethereum mainnet endpoint (you can get free access to one here)

What You Will Do

  • Establish a WebSocket (WSS) connection to an Ethereum RPC endpoint
  • Create and set up the project directory
  • Retrieve & filter Mempool transactions

To learn some background info about Mempool transactions, take a look at our How to access Ethereum Mempool guide written by Sahil Sen!

Establish a Connection to Your QuickNode RPC

To retrieve Mempool transactions, we will need a full node connected to Ethereum mainnet. You can run your own node by looking at the Getting Started page on Geth's documentation. However, this can be hard to manage at times and may not be optimized as well as we'd like. Instead, you can easily spin up an endpoint on QuickNode and have access to 20+ blockchains. QuickNode's infrastructure is optimized for latency and redundancy, making it up to 8x faster than competitors. You can use the QuickNode Compare Tool to benchmark different RPCs against QuickNodes endpoints.

Click the Create the Endpoint button and select the Ethereum mainnet network. Then once your endpoint is ready, keep the WSS Provider URL handy, as you'll need it in the following section.

QuickNode endpoints page

You can view the Metrics tab on your endpoint page to gain valuable insight into which methods you are calling and at what volume. Note that responses received by an open WebSocket connection are counted per response (learn more here).

Setting Up the Project

Now that we have access to the Ethereum network via our WSS Provider URL, we can create our project directory that will hold our script and other dependencies. Open up your terminal and run the following set of commands to create the project directory, install dependencies and create the necessary files:

mkdir filtering-mempool-txns
cd filtering-mempool-txns
npm init -y
npm install ethers
echo > index.js && echo > abi.json

Make sure that your Ethers.js installation is version 6 >=

Open the filtering-mempool-txns directory you just created in a code editor of choice. We'll be using VSCode. Navigate to the abi.json file and paste the following ABI into the file:

[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactOutputParams","name":"params","type":"tuple"}],"name":"exactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactOutputSingleParams","name":"params","type":"tuple"}],"name":"exactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]

The ABI represents the interface for the contract and helps us encode and decode data in and out of machine code (bytecode). For a refresher on what an ABI is, check out our What is an ABI? guide.

Now with our project directory set up, we can create the logic for retrieving and filtering Mempool transactions.

Filtering Mempool Transactions on Ethereum Mainnet

For this guide, we will be filtering through the Mempool to find incoming transactions that want to interact with Uniswap's SwapRouter (which is a "Router for stateless execution of swaps against Uniswap V3"). An example swap transaction would look like this.

A WebSocket URL allows a persistent and real-time connection to an Ethereum endpoint without making multiple HTTP requests. Good practices you may want to implement when using WebSockets include adding logic to automatically reconnect if your server connection disconnects and monitoring the network health of the host you're running your app from (more info can be found here).

Now, let us cover some quick background on how the Mempool transactions we retrieve will look and how we will filter them.

A typical Mempool transaction will look like this:

{
ha: '0x7deacece762e83d20e767dcafa6bfa0cee8e41414465df1e20866b800a3a5d6b',
type: 2,
accessList: [],
blockHa: null,
blockNumber: null,
transactionIndex: null,
confirmations: 0,
from: '0x97a88D526232D228f15621B3bacce9C56137d789',
gasPrice: BigNumber { _hex: '0x09c7d88b48', _isBigNumber: true },
maxPriorityFeePerGas: BigNumber { _hex: '0x59682f00', _isBigNumber: true },
maxFeePerGas: BigNumber { _hex: '0x09c7d88b48', _isBigNumber: true },
gasLimit: BigNumber { _hex: '0x013e33', _isBigNumber: true },
to: '0x37613D64258c0FE09d5E53EeCb091DA5b8fA8707',
value: BigNumber { _hex: '0x00', _isBigNumber: true },
nonce: 190,
data: '0x095ea7b3000000000000000000000000152ec68908f44cf1b3b000b451cf092648efd1bcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
r: '0xa12916d49d6b2983bd9fc991ca0efc3e2c7b7670797f5c69969397df61893069',
s: '0x04b080ff7242d026801978ff25de874741a879665e50efd8c2fb8e3285dfc69c',
v: 1,
creates: null,
chainId: 1,
wait: [Function (anonymous)]
}

Each function in a smart contract has a function signature. You can identify the function signature by looking at the first 4 bytes of the data within the transaction's data field. A byte is considered two hex characters (e.g., 7d).

As for the example above, the first 4 bytes are 095ea7b3 (excluding the leading 0x) which represent an approval function being called on the smart contract address in the To field (i.e., 0x37613D64258c0FE09d5E53EeCb091DA5b8fA8707). The remaining hex characters in the string represent the function parameters and inputs. The 152ec68908f44cf1b3b000b451cf092648efd1bc represents the spender address and ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff decodes to a large uint256 (i.e., 115792089237316...).

In our code logic, we will check the To field on every incoming transaction to see if it matches the Uniswap router address (e.g., 0xE592427A0AEce92De3Edee1F18E0157C05861564). We will also filter the data field to only return transactions calling a specific function. The function we are filtering on is exactInputSingle and its function signature is 0x414bf389. This function can be found in the ABI.json file and is used when swapping one token for the maximum amount of another token.

Now, let's open up the index.js file and input the following code:

const ethers = require("ethers");
const abi = require('./abi.json');

const wssUrl = "QUICKNODE_WSS_URL_PROVIDER";
const router = "0xE592427A0AEce92De3Edee1F18E0157C05861564"; //Uniswap v3 SwapRouter

const interface = new ethers.Interface(abi);

async function main() {
const provider = new ethers.WebSocketProvider(wssUrl);
provider.on('pending', async (tx) => {
const txnData = await provider.getTransaction(tx);
if (txnData) {
let gas = txnData['gasPrice'];
if (txnData.to == router && txnData['data'].includes("0x414bf389")) {
console.log("hash: ", txnData['hash']);
let decoded = interface.decodeFunctionData("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))", txnData['data']);
logTxn(decoded, gas);
}
}
})
}

async function logTxn(data, gas) {
console.log("tokenIn: ", data['params']['tokenIn']);
console.log("tokenOut: ", data['params']['tokenOut']);
console.log("amount: ", data['params']['amountOutMinimum'].toString());
console.log("gasPrice: ", gas.toString());
console.log("-------------");
}

main();

Let's go over the code:

Line 1-2: Import dependencies such as Ethers.js and the contract ABI (from Uniswap's SwapRouter).

Lines 5-6: Declare variables that will hold our WSS Provider URL, SwapRouter contract address, and empty dictionary for when we log transactions.

Line 8: Define an Interface object with Ethers.js that holds the encoding and decoding required to interact with the SwapRouter contract.

Lines 10-22: Declare an async function that initiates a WebSocketProvider object via our WSS URL. Within the function, we use the provider.on('pending') method to retrieve only the pending transactions (i.e., Mempool transactions). Next, we create if statements to ensure that the transaction object is not null and that it filters on the contract address (i.e., router) and function signature (i.e., 0x414bf389). Finally, we use the Ethers.js Interface class to decode the transaction data on transactions that meets our conditional statements.

Lines 24-32: Declare an async function that takes two inputs: the transaction object, data and the gas amount, gas. The function then uses these values to output the tokenIn (the token being swapped), the tokenOut (the token being swapped for), the amountOutMinimum (the output of tokens from the swap), and the gasPrice.

To execute the script, run the following command in your terminal:

node index.js

It can take up several minutes to find a transaction that meets our criteria, but when one is detected, you'll see the following output:

Closing Thoughts

Kudos! You now know how to retrieve and filter Mempool transactions on Ethereum. Try filtering on a different smart contract address and function to solidify your understanding. You can also continue improving the current script by detecting multiple functions or by trying to create an arbitrage or front-run bot.

Join our Discord if you have any questions, or reach out to us via Twitter.

We ❤️ Feedback!

If you have any feedback or questions on this guide, let us know. We'd love to hear from you!

Share this guide