FABRKNT
Building with the Stack — Real-World Rust EVM Apps
Application Patterns
Lesson 2 of 11·CONTENT45 min80 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Building with the Stack — Real-World Rust EVM Apps
Lesson role
CONTENT
Sequence
2 / 11

Lab 1 — Build a Minimal MEV Searcher in Rust

Question

An MEV searcher monitors the mempool, simulates profitable bundles, and submits them to a builder. Build the minimum thing that does this end-to-end. Mempool subscription + revm simulation + bundle submission, ~400 lines of Rust.

Principle (minimum model)

  • Three components. Mempool watcher (Alloy Provider::subscribe_pending_transactions) + revm simulator (fork mainnet at the latest block, simulate the candidate bundle) + bundle submitter (signed JSON-RPC to a builder endpoint).
  • Mempool watcher. provider.subscribe_pending_transactions().await?.into_stream() yields tx hashes. For each, provider.get_transaction(hash) fetches the full tx.
  • Revm simulator. Fork at the latest block (AnvilForkBackend or AlloyDB); run the candidate bundle (your tx + the user's tx) through revm; compute profit by diffing your balance before and after.
  • Bundle submitter. eth_sendBundle JSON-RPC to a builder like Flashbots (or a local builder for testing). The bundle is a list of signed txs with a target block number.
  • Test gate. Forked anvil at a block where a known profitable swap exists; simulate; assert profit > minimum threshold. Deterministic.
  • Why this is the canonical lab. MEV searching is the most common pattern for "watch chain, simulate, submit" — it transfers directly to liquidation bots, oracle keepers, and arbitrage bots.

Worked example + steps

Build a Minimal MEV Searcher in Rust

A greenfield "you'd structure your bot like this" walkthrough lies about the shape of production. Real searchers don't start from main.rs. They start from a framework — and the one to read is Paradigm's artemis, the Rust MEV-bot framework Paradigm open-sourced and continues to dogfood.

Open the repo. Read it. This lesson walks you through it.

📌 Why this is the right starting point. Watching the public mempool, decoding a swap, fork-simulating in Revm, building a Flashbots bundle — every searcher does these things. The interesting question isn't "can you write them once?" It's "how do you organize them so the next strategy you ship isn't a rewrite?" That's exactly the question artemis answers. The MEV logic is yours; the orchestration is borrowed.

Acceptance criteria

The lesson is complete when these tests pass (full code at the end in §Test gate):

  1. finds_known_arb_at_pinned_block — at a pinned mainnet block where a known arb existed, your strategy emits an Action with positive expected P&L.
  2. retracts_action_on_reorg — on a synthetic ChainReorged notification, the strategy retracts any pending Action that depended on the reorged block.

Test-first reading. Skim these now. The walkthrough below explains the types (Strategy<E, A>, Action::SubmitBundle) and patterns (forked Revm, mempool collectors) you'll need to write tests against.

The artemis architecture, in one sentence

A searcher is an event-processing pipeline: external signals come in, MEV logic decides what to do, actions go out. Artemis splits that pipeline into three traits and an engine that wires them together.

ComponentTraitWhat it does
CollectorCollector<E>External world → internal event E. Pending tx, new block, marketplace order, MEV-Share hint — each gets its own collector.
StrategyStrategy<E, A>Event E → zero or more actions A. This is the MEV brain. The only file you actually write per opportunity.
ExecutorExecutor<A>Action A → side effect. Flashbots bundle submit, public-mempool send, off-chain order post.

Step 1: Open the traits

The whole core abstraction is one file, ~120 lines: crates/artemis-core/src/types.rs. Open it now.

#[async_trait]
pub trait Collector<E>: Send + Sync {
    async fn get_event_stream(&self) -> Result<CollectorStream<'_, E>>;
}

#[async_trait]
pub trait Strategy<E, A>: Send + Sync {
    async fn sync_state(&mut self) -> Result<()>;
    async fn process_event(&mut self, event: E) -> Vec<A>;
}

#[async_trait]
pub trait Executor<A>: Send + Sync {
    async fn execute(&self, action: A) -> Result<()>;
}

That's the entire contract. Three methods. Two generic params (E for events, A for actions). Everything else in the framework — engine, channels, mappers — is plumbing around these three.

🔍 Find in repo. In that same file, find CollectorMap and ExecutorMap. Read 30 seconds. In your own words: what does CollectorMap solve that you'd otherwise have to solve by writing a new Collector?

Step 2: How events flow — read the engine

Open crates/artemis-core/src/engine.rs. The Engine<E, A> struct holds three Vec<Box<dyn …>> — one each for collectors, strategies, executors. The run method spawns one Tokio task per component and connects them with two tokio::sync::broadcast channels:

collectors -- events --> [event channel] -- events --> strategies
                                                          |
                                                         actions
                                                          v
executors <-- actions <-- [action channel] <-- actions <--+

Broadcast — so every strategy sees every event; every executor sees every action. Strategies that don't care for a given event return vec![]. Executors that don't care for a given action filter via ExecutorMap.

The whole point: you ship a new strategy by writing one impl Strategy and calling engine.add_strategy(...). Collectors and executors are reused.

Step 3: Find the real Collectors and Executors

Don't trust the abstraction until you've seen concrete implementors. Open these and skim:

  • crates/artemis-core/src/collectors/: mempool_collector.rs (subscribe to pending txs), block_collector.rs (new heads), mevshare_collector.rs (private hint stream), opensea_order_collector.rs (NFT marketplace), log_collector.rs (filtered log subscription).
  • crates/artemis-core/src/executors/: mempool_executor.rs (public submit), flashbots_executor.rs (bundle to Flashbots relay), mev_share_executor.rs (MEV-Share submission).

Each file is small — ~50-100 lines. Open mempool_collector.rs specifically:

#[async_trait]
impl<M> Collector<Transaction> for MempoolCollector<M>
where
    M: Middleware,
    M::Provider: PubsubClient,
{
    async fn get_event_stream(&self) -> Result<CollectorStream<'_, Transaction>> {
        let stream = self.provider.subscribe_pending_txs().await?;
        let stream = stream.transactions_unordered(256);
        let stream = stream.filter_map(|res| async move { res.ok() });
        Ok(Box::pin(stream))
    }
}

That's the entire mempool collector. The subscribe_pending_txs().transactions_unordered(256) pattern materializes tx bodies in parallel — concurrency 256 — instead of one-by-one. Note what's not in this file: no MEV logic, no decoding, no strategy concerns. A collector's job is to push a typed stream upstream and shut up.

Step 4: A real Strategy — opensea-sudo-arb

Now the payoff. Open crates/strategies/opensea-sudo-arb/. This is the one strategy shipped in the artemis tree — atomic cross-market NFT arb between OpenSea (Seaport) and Sudoswap (LSSVM pools).

Two files matter:

  • src/types.rs — defines this strategy's Event and Action enums.
  • src/strategy.rs — the impl Strategy<Event, Action> for OpenseaSudoArb.

The Event is just:

pub enum Event {
    NewBlock(NewBlock),
    OpenseaOrder(Box<OpenseaOrder>),
}

Two collectors feed this strategy: a block collector and an OpenSea order collector. That's it.

The process_event body is the whole MEV decision:

async fn process_event(&mut self, event: Event) -> Vec<Action> {
    match event {
        Event::OpenseaOrder(order) => self
            .process_order_event(*order).await
            .map_or(vec![], |a| vec![a]),
        Event::NewBlock(block) => match self.process_new_block_event(block).await {
            Ok(_) => vec![],
            Err(e) => { panic!("Strategy is out of sync {}", e); }
        },
    }
}

process_order_event: a new NFT listing arrived on OpenSea — is there a Sudoswap pool willing to pay more than the listing price for that NFT? If yes, return an Action::SubmitTx for an atomic arb contract that buys on OpenSea and sells into the Sudo pool in one tx.

process_new_block_event: scan the new block's logs for Sudo pool state changes (buys/sells/spot-price updates) and refresh the internal pool_bids map. No actions; just state hygiene.

🔍 Find in repo. In the same strategy.rs file, find sync_state. Read it. Predict: why does it need to enumerate every Sudo pool ever deployed before the strategy can start? What breaks if you skip it?

The arb contract itself is a separate piece of Solidity (contracts/src/SudoOpenseaArb.sol) — the strategy's job is to detect the opportunity and shape the calldata; the on-chain contract does the atomic buy-sell.

Step 5: Now answer the Step 1 predict

Why is Executor separate from Strategy? Because the same MEV opportunity can be submitted three different ways depending on chain conditions: public mempool (cheap, exposed), Flashbots bundle (private, atomic), MEV-Share (semi-private, partial-reveal). A strategy that emits a typed Action and lets the engine route to whichever executor is healthy today is resilient. A strategy that hardcodes flashbots_relay.send_bundle(...) dies the day Flashbots is degraded.

The trait split isn't theoretical cleanliness — it's about swapping submission paths without touching MEV logic.

(Answer: implement Collector<OpenseaOrder> — or Collector<Event> directly via CollectorMap — and register it on the engine. Zero changes to strategy.rs. That's the architecture working.)

Step 6: From reading to shipping — your own bot

If you wanted to ship a 2-hop Uniswap arb searcher (the classic) on Tempo or MegaETH using artemis:

  1. Reuse: MempoolCollector for pending swaps, BlockCollector for new heads, FlashbotsExecutor (or your L1's equivalent bundle endpoint). All as-is.
  2. Write: a single UniArbStrategy with Event = { NewBlock, PendingTx } and Action = { SubmitBundle }. Inside process_event on PendingTx: decode the swap, fork-simulate in Revm, detect the cross-pool spread, build the bundle. Inside process_event on NewBlock: refresh reserves cache, drop stale opportunities.
  3. Plumb: engine.add_collector(...) × 2, engine.add_strategy(UniArbStrategy::new(...)), engine.add_executor(...), engine.run().await.

The MEV-logic surface is one file. Everything else is borrowed.

The honest comparison — artemis vs subway

Artemis acknowledges its lineage. Read the README's Acknowledgements: subway, subway-rs, rusty-sando. These are fully-baked MEV bots — subway specifically is a TypeScript sandwich bot, shipped end to end, opinionated about what MEV you're doing.

Artemis is the opposite: a framework with one example strategy. Subway tells you what to run. Artemis tells you how to organize whatever you choose to run.

subwayartemis
ShapeTurnkey sandwich botFramework + one example strategy
LanguageTypeScriptRust
CustomizationFork and rewriteImplement traits
What you supplyAPI keys, capitalMEV logic, capital
Right choice whenYou want sandwiching, today, as a learning artifactYou're shipping a strategy that doesn't exist yet

If you're reading this lesson, you're shipping the latter. Artemis is your scaffolding.

Recall checklist

Before moving on, confirm you can answer each of these without scrolling back:

  1. Name the three artemis traits and what each consumes/produces.
  2. Where in the codebase does cross-strategy coordination live? (Trap question — see Step 2.)
  3. Why is the Action enum strategy-local rather than framework-wide?
  4. In opensea-sudo-arb, what does process_new_block_event do that process_order_event doesn't?
  5. To swap submission from public mempool to Flashbots, what changes in the Strategy implementation? (Answer: nothing — you change the registered Executor.)

If you stumbled on 2 or 4, re-read Steps 2 and 4 before the next lesson.

Drill

  1. Map out a new strategy. Pick a real MEV opportunity (Uniswap V3 JIT liquidity, Curve cross-pool arb, perp-funding-rate arb). On paper: what Event variants does it need? What Action variants? Which existing collectors/executors can you reuse? (30 min)
  2. Trace one event end-to-end. From a mainnet pending tx hitting MempoolCollector::get_event_stream to a hypothetical SubmitTxToMempool action being executed, list every .await point in the artemis codebase. There are fewer than you think. (45 min)
  3. Backport a collector. Pick one collector in crates/artemis-core/src/collectors/ and translate it from ethers-rs to Alloy 1.x. The trait signature doesn't change; only the underlying provider does. (2 hours)
  4. Read the run loop. Open engine.rs again. The run method spawns collectors.len() + strategies.len() + executors.len() tasks. Walk the message-flow: how does an event from collector A reach strategy B's process_event? Name the channel type and the receiver. (30 min)
  5. Build a stub strategy on top. Clone artemis, add a new module under crates/strategies/ that implements Strategy<Event, Action> where process_event just logs the event. Wire it into a minimal binary that runs MempoolCollector + your stub + a no-op executor. cargo run it against a free WS endpoint. (3 hours)

Finish drill 5 and you have a working artemis-based searcher skeleton ready for whatever MEV logic you want to ship into it.

Test gate

Per Test gate — every app in this tier ships with passing tests, this lesson's minimum gate is two tests in your stub strategy from drill 5:

  1. Forked-state opportunity replay. Pin a block where a known arb existed (Etherscan + EigenPhi will surface candidates). Build your strategy against an AlloyDB-backed Revm at that block. Assert the strategy emits an Action with positive expected P&L.
  2. Reorg integrity. Feed a synthetic ChainReorged notification through your collector and assert the strategy retracts any pending Action that depended on the reorged block. (Ignoring reorgs is the most common production failure mode in MEV systems — you submit a bundle, the chain reorgs, your accounting still claims you won.)

Sketch:

// tests/integration.rs
const PINNED_BLOCK: u64 = 18_500_000;  // a block with a known Uniswap V3 / Curve arb
const FORK_RPC: &str = "https://eth.merkle.io";

#[tokio::test]
async fn finds_known_arb_at_pinned_block() {
    let provider = forked_provider_at(FORK_RPC, PINNED_BLOCK).await;
    let strategy = MyArbStrategy::new(provider);
    let event = Event::NewBlock { number: PINNED_BLOCK };
    let actions = strategy.process_event(event).await;
    let arb = actions.iter().find(|a| matches!(a, Action::SubmitBundle { .. }));
    assert!(arb.is_some(), "should have found the known arb");
    assert_pnl_positive(arb.unwrap());
}

#[tokio::test]
async fn retracts_action_on_reorg() {
    // submit synthetic block N, then reorg over N; assert the action queue is empty
}

The lesson is not complete until both tests are green. If you can cargo run your searcher against mainnet but cannot cargo test it, you have a demo, not a deliverable.

📺 Further watching

vCCYFSAdCFo | Understanding MEV — Georgios Konstantopoulos, Dan Robinson, Hasu (Paradigm)

What comes next in this tier

The full Building with the Stack tier ships ten lessons end to end. From here:

  • Lesson 2 — Reorg-aware Postgres indexer (ExEx-driven, in-process)
  • Lesson 3 — Custom RPC endpoint via extend_rpc_modules
  • Lesson 4 — Wallet backend (signer pool + nonce manager + replace-on-stuck)
  • Lesson 5 — EIP-7702 sponsor service (Type 4 tx + paymaster pattern)
  • Lesson 6 — Foundry-style cheatcode (custom precompile + minimal harness)
  • Lesson 7 — Swap aggregator (Revm fork + cross-venue quotes)
  • Lesson 8 (Capstone) — Frontrun-resistant order router that integrates everything above
  • Lesson 9 — Validate-revm cross-client harness (compare your sim against a production provider)
  • Lesson 10 — HTTP 402 / MPP machine-payments endpoint (Tempo's payments stack)

Each is a self-contained ~200–300 line build with the same predict / find-in-repo / anti-fluency style. Pick the one that maps to your target use case.

🧭 Where you are now in the stack: you've shipped a networking × concurrency application — event-driven pipeline (artemis collectors → strategies → executors) applied to MEV, with the test gate (forked-state arb replay + reorg integrity) locking in correctness. Same shape Kafka Streams and HFT order handlers ship. Next lesson moves to the database layer: a reorg-aware indexer driven by ExEx.

Summary (3 lines)

  • Minimal MEV searcher = mempool watcher + revm simulator + bundle submitter, ~400 lines of Rust. End-to-end loop in three Alloy + revm calls.
  • Test gate = forked anvil at a known-profitable block; assert profit > threshold. Deterministic.
  • "Watch chain, simulate, submit" is the canonical pattern; transfers to liquidation bots / oracle keepers / arbitrage bots. Next: ExEx indexer.