The legends at CoW Swap and beaverbuild launched a private RPC service in April called MEVBlocker. You can read the MEVBlocker launch announcement on Twitter for more details.
MEVBlocker is similar to existing private RPCs like SecureRPC and Flashbots Protect.
Their value proposition is that users can submit private transactions, which are shared with MEV searchers. Searchers can propose bundles, which are shared with the builder, and a portion of the bribe is redistributed back to the original user.
A bundle submitted to MEVBlocker can only hold two transactions, with the original transaction in the first position. Further, the transactions shared with searchers have the signatures stripped. Without the user’s signature, the raw transaction cannot be reconstructed and thus cannot be re-bundled and submitted to another builder.
The MEVBlocker searcher documentation is laughably terse, so searching from the feed requires a lot of self-study. I built a searcher bot for this feed roughly a month ago, and have been operating it with some modest success. It has confirmed roughly 125 successful backruns, profiting ~0.1 ETH. It’s not crazy stuff, but consider that the MEVBlocker feed is relatively low volume, and non-toxic backrun arbitrage profit potential is necessarily lower than combined frontrunning and backrunning (sandwich) MEV.
Even if you don’t make much money from this specific example, integrating with MEVBlocker allows you to learn a few useful things that will ease your integration with other private searcher feeds.
Degen Code Caveat: hundreds of other readers now have access to this opportunity with very little effort, so you’re not likely to make much profit. Per my usual policy, I will turn off my bot after publishing.
Watching the MEVBlocker Feed
The MEVBlocker feed is available at wss://searchers.mevblocker.io.
Connect to it and observe the feed with a small example script:
mevblocker_watcher.py
import asyncio
import logging
import ujson
import websockets.client
import websockets.exceptions
MEVBLOCKER_FEED_URI = "wss://searchers.mevblocker.io"
async def watch_mevblocker_feed():
async for websocket in websockets.client.connect(
uri=MEVBLOCKER_FEED_URI,
max_queue=None,
):
try:
await websocket.send(
ujson.dumps(
{
"method": "eth_subscribe",
"params": [
"mevblocker_partialPendingTransactions"
],
}
)
)
subscription_id = ujson.loads(await websocket.recv())[
"result"
]
except Exception:
continue
logger.info(
f"Subscription Active: MEVBlocker Feed - {subscription_id}"
)
try:
async for message in websocket:
logger.info(ujson.loads(message)["params"]["result"])
except asyncio.exceptions.CancelledError:
return
except websockets.exceptions.ConnectionClosed:
logger.info("Reconnecting to MEVBlocker Feed...")
continue
if __name__ == "__main__":
logger = logging.getLogger("mevblocker_watcher")
logger.propagate = False
logger.setLevel(logging.INFO)
logger_formatter = logging.Formatter("%(levelname)s - %(message)s")
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logger_formatter)
logger.addHandler(stream_handler)
asyncio.run(watch_mevblocker_feed())
Watch the feed for a bit and observe the transactions:
INFO - {'nonce': '0x27e1', 'gasPrice': '0xdef01d03b', 'gas': '0x5208', 'to': '0xe4a18738a5e2688b4411bc28076c142146e87d55', 'value': '0x156209c9d597a2', 'data': '0x', 'hash': '0x61284e27bbd8421673fa3a6b751d598084770248a64dd63acf6f14953f699f03', 'from': '0xafd99a1a7e2195a8e0fdb6e8bd45efdff15feadd'}
INFO - {'chainId': '0x1', 'to': '0x881d40237659c251811cec9c364ef91dc08d300c', 'value': '0x1bc16d674ec80000', 'data': '0x5f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000c307846656544796e616d6963000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fc10cd3895f2c66d6639ec33ae6360d6cfca7d6d0000000000000000000000000000000000000000000000001b83413f0b364000000000000000000000000000000000000000000000000006a0a441158b363e400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000003e2c284391c000000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f191500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000128d9627aa400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000001b83413f0b364000000000000000000000000000000000000000000000000006a0a441158b363e4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000fc10cd3895f2c66d6639ec33ae6360d6cfca7d6d869584cd00000000000000000000000011ededebf63bef0ea2d2d071bdf88f71543ec6fb000000000000000000000000000000004f9d9760c7ddd5f2396236f0a7bbcc380000000000000000000000000000000000000000000000000092', 'accessList': [], 'nonce': '0x3d', 'maxPriorityFeePerGas': '0x0', 'maxFeePerGas': '0x94b341097', 'gas': '0x45abc', 'type': '0x2', 'hash': '0x0bdcded17cc507a45cb948c6a0abaad9afa5fa9dee219862a24999f5db58e87d', 'from': '0x8e90741dc04c6ce6b8a56f6b42bcc9a400cfcbd2'}
INFO - {'chainId': '0x1', 'to': '0x4e69a51f24f5a46919113cc78ab262da74a4611d', 'value': '0x2c68af0bb140000', 'data': '0x90fec7e70000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c68af0bb140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b142513a8adf700c77b6492aef8d08b35eceae080000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f8fc0b12c6e9e37ee4785d9011e3e3f855187a5900000000000000000000000000000000000000000000000000000000000000030000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d0000000000000000000000005c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f0000000000000000000000005c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f', 'accessList': [], 'nonce': '0x2b', 'maxPriorityFeePerGas': '0x0', 'maxFeePerGas': '0x2eac716e1e', 'gas': '0x66a8c', 'type': '0x2', 'hash': '0x46e935e2ac25690643ec1168542da05dbf9a03e15c25034483fd09ba5f3ab8cd', 'from': '0xf8fc0b12c6e9e37ee4785d9011e3e3f855187a59'}
INFO - {'chainId': '0x1', 'to': '0xbdb241f7fd4ed7f0f40fc6c26b7c1f8dd21b6c49', 'value': '0x0', 'data': '0xad6b06cd0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d0000000000000000000000003f9745e3a4be06d567dccc7af9c20f6fee12c3ff0000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000004758d53650a7dc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000a217b21de0900000000000000000000000000006278346671fee03a4ef7d7d85a2b76000ed9e44800000000000000000000000000000000000000000000000000000000000000020000000000000000000000003f9745e3a4be06d567dccc7af9c20f6fee12c3ff000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 'accessList': [], 'nonce': '0x310', 'maxPriorityFeePerGas': '0x0', 'maxFeePerGas': '0x174876e800', 'gas': '0x7a120', 'type': '0x2', 'hash': '0xd694a31cc5997ed3d7f9541b5ec4ca8269de3eeb114714b8a68be1f1f9357d94', 'from': '0x86bc952b086ca2277679a7c68199d19c15d602c2'}
INFO - {'chainId': '0x1', 'to': '0x043f64a4add524457f22b85fe8ee58baf0edec02', 'value': '0x0', 'data': '0x1eec24120000000000000000000000000000000000000000000000000000000000002710', 'accessList': [], 'nonce': '0xb3', 'maxPriorityFeePerGas': '0x0', 'maxFeePerGas': '0xd721300db', 'gas': '0x6831d', 'type': '0x2', 'hash': '0x561b7b91e8fe71d7e3a2b503b038b07c28da39cd27c72312b960308a94588430', 'from': '0xc1f2b71a502b551a65eee9c96318afdd5fd439fa'}
INFO - {'chainId': '0x1', 'to': '0x1111111254eeb25477b68fb85ed929f73a960582', 'value': '0x0', 'data': '0x12aa3caf000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000008390a1da07e376ef7add4be859ba74fb83aa02d50000000000000000000000004507cef57c46789ef8d1a19ea45f4216bae2b528000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd0900000000000000000000000074d0116552c7a52059b12c0d251976a3405c7806000000000000000000000000000000000000000000000000000aa87bee5380000000000000000000000000000000000000000000000000000001aea0827993d3000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c90000000000000000000000000000000000000000000000000000000001ab00a007e5c0d200000000000000000000000000000000000000000000000000018700012000a0c9e75c4800000000000000001d150000000000000000000000000000000000000000000000000000f200008f0c208390a1da07e376ef7add4be859ba74fb83aa02d569c66beafb06674db41b22cfc50c34a93b8d82a26ae40711b8002dc6c069c66beafb06674db41b22cfc50c34a93b8d82a2c7e6b676bfc73ae40bcc4577f22aab1682c691c6000000000000000000000000000000000000000000000000311537a5041a89668390a1da07e376ef7add4be859ba74fb83aa02d502a00000000000000000000000000000000000000000000000004396a742f42838a1ee63c1e58166ba59cbd09e75b209d1d7e8cf97f4ab34da413b8390a1da07e376ef7add4be859ba74fb83aa02d5c7e6b676bfc73ae40bcc4577f22aab1682c691c600206ae4071138002dc6c0c7e6b676bfc73ae40bcc4577f22aab1682c691c61111111254eeb25477b68fb85ed929f73a9605820000000000000000000000000000000000000000000000000001ad29b67adca3c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000e26b9977', 'accessList': [], 'nonce': '0x335', 'maxPriorityFeePerGas': '0x0', 'maxFeePerGas': '0x1c12cbf7a8', 'gas': '0x6baf2', 'type': '0x2', 'hash': '0x0e2566373c72c3f143fbdc451f516456a40e3f915b44d0583f4260df768afa96', 'from': '0x74d0116552c7a52059b12c0d251976a3405c7806'}
INFO - {'chainId': '0x1', 'to': '0x38f935e58ee761352aa40c30284c4a4ac6596dc9', 'value': '0x1bc16d674ec80000', 'data': '0x9fc0859d000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000028c02aaa9aa61471a70db4277db75da7b3c5fa3d7824d73b693fee4c8c1923a4f55b31ad7880648f6d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000003b6579b7ec1cd629cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000561148000000000000000000000000000000000000000000000000000000000015845200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000014e0eb8e77e3b295a1bd4fe72d2156df8879e7b4e9000000000000000000000000', 'accessList': [], 'nonce': '0xa85', 'maxPriorityFeePerGas': '0x0', 'maxFeePerGas': '0xf098538b4', 'gas': '0x4fadb', 'type': '0x2', 'hash': '0x2893faf17efd89da9c39ee7644c63f0e48828173b835e80bbacb35877169ae91', 'from': '0xe0eb8ed4f7851a8bbaf59c1fb1e1a1338068e553'}
[...]
We’ve seen transactions like this before, so working with them is largely a rehash of previous lessons. I recommend reviewing the Uniswap Router Backrunner project if you need an in-depth exploration of decoding and simulating router transactions.
MEVBlocker Feed Gotchas
There are some odd aspects of the MEVBlocker feed that will cause you frustration.
Fake Transactions
If a transaction is broadcast over the Ethereum mempool, you know it’s validly formed at minimum. It may revert or have impractical gas values, but you can inspect and simulate it without worry.
However, some of the transactions you’ll see from MEVBlocker are invalid. They will have incorrect nonces, include malformed calldata, come from EOAs with a balance insufficient to pay for gas, have conflicting transaction types, etc.
It is very good practice to observe all these bad transactions and discard them without crashing your bot! I’ve even seen transactions with incorrect chain IDs (recall that mainnet = 1), so you have to view every part of the transaction with suspicion.
If some of the validation code looks extra paranoid, know that it’s there because of some bad transaction that caused me trouble.
You might find more, so please share!
Non-check-summed Addresses
All addresses in the MEVBlocker feed are lowercase, and thus not check-summed. Web3py will throw warnings and exceptions if you provide a non-check-summed address to various functions.
All MEVBlocker feed addresses need to be check-summed in-place. Use the to_checksum_address
function provided by eth_utils for this.
Hex vs Integer Values
Web3py expects integers for certain values provided as part of a transaction dictionary. You can review the defaults HERE.
If you look to the feed results above, you’ll see that values for chainId
, nonce
, gas
, type
, and more, are provided as a hex string. Web3py expects integers, so you need to tidy up the transaction dictionary before doing any web3py operations on it.
To convert these values in-place, you can loop through the values that cannot remain hex-formatted:
for key in (
"gas",
"gasPrice",
"maxFeePerGas",
"maxPriorityFeePerGas",
"nonce",
"value",
):
try:
transaction[key] = int(transaction[key], 16)
except KeyError:
pass
The try/except
block is necessary because some transactions won’t have all of the keys.
Executor Contract Improvements
This project includes an updated contract. It changes how bribes are delivered to the validator. It now sends the block.coinbase
bribe as a percentage of the real WETH profit. While submitting traditional bundles (not MEVBlocker), I observed some of my bundles were being ordered later in the block. When this occured with a V3 pool as the last hop (specifying an exact input amount), the WETH profit could be lower-than-expected, but still positive. When this occurs, the transaction could still be bundled, but would not result in the expected profit after fees.
Sending the bribe as priority fee is an immutable choice, since it cannot not be adjusted during execution by the contract.
To mitigate this, I changed the approach to send the bribe as msg.value
and include a profit check that reduces the delivered bribe if this “last swap slippage” occured.
It’s not perfect, and negative profit transactions can still occur if the resulting WETH delta is below some threshold. But this is an improvement over the naive “send the profit up front” approach.
I will return to the executor contract topic soon, since Vyper has included several nice improvements in recent releases.
You do not have to use this updated contract, but be aware that you will need to adjust the build_tx
function to supply the correct parameters to your function, and adjust the value and priority fee calculation in the send_to_mevblocker
function.
The updated smart contract accepts a new parameter called bribe_bips
, which reflects the percentage of profit delivered to the validator. The expected profit must be sent as msg.value
, and that bribe can be reduced dynamically depending on actual profit: