Skip to main content

Install MEV Sidecar - Podman

Install the MEV Sidecar as a Podman container managed by a user-scoped Quadlet unit running under a dedicated fastlane user. This guide assumes a Monad node is running and synchronized on the validator's machine, and that Validator Setup has been completed.

This guide prioritizes isolation and supply chain verification. The sidecar only needs access to monad-bft's mempool socket, so the socket is relocated out of its default location (which sits alongside unrelated validator files) into a dedicated IPC directory accessible only to a separate fastlane user. The container itself runs read-only, with all Linux capabilities dropped and resource limits applied. Every release is pinned by image digest after three independent checks: GPG signature on the release manifest, cosign image signature, and SLSA provenance attestation.

Minimum Version Requirement

The MEV Sidecar requires monad-bft v0.12.6 or later. If you are running an older version, the sidecar will connect but tx_received will remain at 0. Upgrade monad-bft before proceeding.

Dependencies

Install podman and utilities.

sudo apt update && sudo apt install -y podman acl curl gnupg systemd-container

Install cosign and gh, it will be used to verify container images.

# cosign. apt where packaged (Ubuntu 25.04+, Debian 13+); fall back to the
# upstream binary on older releases (Ubuntu 22.04/24.04 LTS, Debian 12).
sudo apt install -y cosign || {
sudo curl -L -o /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
sudo chmod +x /usr/local/bin/cosign
}

# gh from GitHub's apt repo. distro gh predates `gh attestation verify` (2.49+).
(type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& sudo mkdir -p -m 755 /etc/apt/sources.list.d \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& sudo apt update \
&& sudo apt install gh -y

Create a dedicated user

Create the fastlane user to scope the sidecar permissions.

# Dedicated user for the sidecar
sudo useradd -m -s /bin/bash fastlane

# Create .env (replace with testnet if needed)
echo "SIDECAR_NETWORK=mainnet" | sudo tee /home/fastlane/.env
sudo chown fastlane:fastlane /home/fastlane/.env

# IPC directory
sudo install -d -o monad -g monad -m 2750 /var/run/monad-ipc

# ACLs: grant fastlane traverse on the dir + default rw on new sockets created inside
sudo setfacl -m u:fastlane:rx /var/run/monad-ipc
sudo setfacl -d -m u:fastlane:rw /var/run/monad-ipc

# Delegate cpu/cpuset cgroup controllers to user.slice so Quadlet's AllowedCPUs= can apply
sudo mkdir -p /etc/systemd/system/[email protected]
sudo tee /etc/systemd/system/[email protected]/delegate.conf <<'EOF'
[Service]
Delegate=cpu cpuset io memory pids
EOF
sudo systemctl daemon-reload

# Enable lingering
sudo loginctl enable-linger fastlane

Update monad config

monad-bft and monad-rpc need to point the mempool IPC socket to a segregated location, which the fastlane user can safely access. This means changing the --mempool-ipc-path flag for monad-bft and the --ipc-path flag for monad-rpc to point at /var/run/monad-ipc/mempool.sock.

caution

A [Service] drop-in that sets ExecStart= replaces the existing command, it does not extend it. The new ExecStart= line must contain every argument the service was originally launched with, not just the flag you want to change. The examples below use one mainnet validator's specific flag set, your set will differ.

View your full original units before editing, and copy the entire ExecStart= block from each (it usually spans multiple lines via \ continuations):

systemctl cat monad-bft
systemctl cat monad-rpc

The captured block can be pasted into the override as-is with its line continuations, or collapsed onto a single ExecStart= line. Lines starting with # inside the command are stripped by systemd as comments, so do not use them to annotate the ExecStart body.

sudo systemctl edit monad-bft

In the editor that opens, paste the override below. The ExecStart=... line must match your original monad-bft ExecStart (from systemctl cat monad-bft), with --mempool-ipc-path changed to /var/run/monad-ipc/mempool.sock. A complete override on a typical mainnet validator looks like this, your paths and flag set might differ:

[Service]
# /var/run is tmpfs, recreate the IPC dir + ACLs on every boot, before monad-bft
# binds its socket.
ExecStartPre=+/bin/bash -c '\
install -d -o monad -g monad -m 2750 /var/run/monad-ipc && \
setfacl -m u:fastlane:rx /var/run/monad-ipc && \
setfacl -d -m u:fastlane:rw /var/run/monad-ipc'

# UMask 0007 keeps the socket's group bits at rwx, so when the kernel inherits
# the directory's default ACL onto the new socket, the file-level mask stays at
# rwx instead of being clamped to r-x, letting u:fastlane:rw take effect.
UMask=0007

ExecStart=
ExecStart=/usr/local/bin/monad-node \
--secp-identity /home/monad/monad-bft/config/id-secp \
--bls-identity /home/monad/monad-bft/config/id-bls \
--node-config /home/monad/monad-bft/config/node.toml \
--forkpoint-config /home/monad/monad-bft/config/forkpoint/forkpoint.toml \
--wal-path /home/monad/monad-bft/wal \
--mempool-ipc-path /var/run/monad-ipc/mempool.sock \
--control-panel-ipc-path /home/monad/monad-bft/controlpanel.sock \
--statesync-ipc-path /home/monad/monad-bft/statesync.sock \
--ledger-path /home/monad/monad-bft/ledger \
--triedb-path /dev/triedb \
--otel-endpoint "http://127.0.0.1:4317" \
--statesync-sq-thread-cpu 8 \
--record-metrics-interval-seconds 1 \
--validators-path /home/monad/monad-bft/config/validators/validators.toml \
--persisted-peers-path /home/monad/monad-bft/config/peers.toml \
--keystore-password "${KEYSTORE_PASSWORD}"

Save and exit the editor.

sudo systemctl edit monad-rpc

In the editor that opens, paste the override below. The ExecStart=... line must match your original monad-rpc ExecStart (from systemctl cat monad-rpc), with --ipc-path changed to /var/run/monad-ipc/mempool.sock. A complete override on a typical mainnet validator looks like this, your paths and flag set might differ:

[Service]
ExecStart=
ExecStart=/usr/local/bin/monad-rpc \
--ipc-path /var/run/monad-ipc/mempool.sock \
--triedb-path /dev/triedb \
--otel-endpoint "http://0.0.0.0:4317" \
--allow-unprotected-txs \
--node-config /home/monad/monad-bft/config/node.toml

Save and exit the editor.

sudo systemctl daemon-reload

# Restart all monad services
sudo systemctl restart monad-bft monad-execution monad-rpc

# Confirm all is running properly
sudo systemctl status monad-bft monad-execution monad-rpc --no-pager -l

Install the sidecar

Run the following commands as the fastlane user.

sudo machinectl shell fastlane@

Import FastLane's release public key.

curl -sL https://fastlane.xyz/keys/sidecar-release.asc | gpg --import

Create the Quadlet file.

mkdir -p /home/fastlane/.config/containers/systemd
nano /home/fastlane/.config/containers/systemd/fastlane-sidecar.container

Paste the following in the editor that opens, and save.

[Unit]
Description=FastLane sidecar
After=network-online.target

[Container]
ContainerName=fastlane-sidecar
Image=ghcr.io/fastlane-labs/fastlane-sidecar:${SIDECAR_VERSION}@${SIDECAR_DIGEST}

ReadOnly=true
DropCapability=ALL
NoNewPrivileges=true

Volume=/var/run/monad-ipc:/var/run/monad-ipc
PublishPort=127.0.0.1:8765:8765

PodmanArgs=--memory=512m --memory-swap=512m --pids-limit=256

Exec=--log-level=info --network=${SIDECAR_NETWORK} --monitoring-port=8765 --txpool-socket=/var/run/monad-ipc/mempool.sock

[Service]
Restart=always
EnvironmentFile=/home/fastlane/.env
AllowedCPUs=12-15

[Install]
WantedBy=default.target

Verify GPG signature.

SIDECAR_VERSION=0.0.16 # Set this to the version you want to install
TAG=v$SIDECAR_VERSION
BASE="https://github.com/FastLane-Labs/fastlane-sidecar/releases/download/${TAG}"

# Download manifest + signature
curl -fLO "${BASE}/fastlane-sidecar-${TAG}.txt"
curl -fLO "${BASE}/fastlane-sidecar-${TAG}.txt.asc"

# Verify the signature
gpg --verify "fastlane-sidecar-${TAG}.txt.asc" "fastlane-sidecar-${TAG}.txt"

# Pin to the GPG-attested digest (NOT by tag, tags can move, digests can't)
DIGEST=$(grep '^Digest:' "fastlane-sidecar-${TAG}.txt" | awk '{print $2}')

# Set image and digest in .env for the Quadlet unit to pick up (idempotent, re-run it on every upgrade)
sed -i \
-e "s|^SIDECAR_VERSION=.*|SIDECAR_VERSION=${SIDECAR_VERSION}|" \
-e "s|^SIDECAR_DIGEST=.*|SIDECAR_DIGEST=${DIGEST}|" \
/home/fastlane/.env
grep -q "^SIDECAR_VERSION=" /home/fastlane/.env || echo "SIDECAR_VERSION=${SIDECAR_VERSION}" >> /home/fastlane/.env
grep -q "^SIDECAR_DIGEST=" /home/fastlane/.env || echo "SIDECAR_DIGEST=${DIGEST}" >> /home/fastlane/.env

Verify image.

IMAGE="ghcr.io/fastlane-labs/fastlane-sidecar@${DIGEST}"

# Image verification
cosign verify "$IMAGE" \
--certificate-identity-regexp "^https://github\.com/FastLane-Labs/fastlane-sidecar/\.github/workflows/publish-docker\.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"

Verify provenance.

# Provenance verification
gh attestation verify "oci://$IMAGE" --owner FastLane-Labs

# If you do not wish to login to gh, run this instead
cosign verify-attestation "$IMAGE" \
--type slsaprovenance1 \
--certificate-identity-regexp "^https://github\.com/FastLane-Labs/fastlane-sidecar/\.github/workflows/publish-docker\.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"

Run the sidecar.

systemctl --user daemon-reload
systemctl --user start fastlane-sidecar.service

Confirm the sidecar is running properly.

curl http://localhost:8765/health | jq
  • last_received_at - Must be a recent timestamp, indicating the sidecar is functioning properly
  • tx_received - Number of transactions the sidecar received from monad-bft. Must be non-zero to be considered healthy
note

monad-bft gets transactions from the network when the validator is about to become the next leader (as per Monad design), therefore tx_received can show 0 for a few minutes before it starts increasing.

Inspect logs.

podman logs -f fastlane-sidecar

Operations

Logs.

podman logs -f fastlane-sidecar
# or
journalctl --user -u fastlane-sidecar -f

Start/Stop.

systemctl --user start fastlane-sidecar.service
systemctl --user stop fastlane-sidecar.service

Status.

systemctl --user status fastlane-sidecar

Upgrading

Open a session with the fastlane user.

sudo machinectl shell fastlane@

Re-run the GPG verify block from above with the new SIDECAR_VERSION (writes new SIDECAR_VERSION + SIDECAR_DIGEST into /home/fastlane/.env).

Restart the container.

systemctl --user restart fastlane-sidecar.service

Confirm.

podman inspect fastlane-sidecar --format '{{.Image}}'
curl http://localhost:8765/health | jq