Measures, for the same swap sent from two wallets at the same instant down two submission paths on BNB Smart Chain — the public mempool and the blink private relay — three things:
- (a) backrun opportunity — how much extractable value our swap left on the table (price dislocation it created), and whether a backrun actually captured it;
- (b) inclusion time — how fast each path landed (blocks waited, in-block index, wall-clock latency);
- (c) frontrun — whether our swap got front-run / sandwiched.
The only intended difference between the two txs is the submission path. Same swap, same amount, same path, identical gas price, fired simultaneously.
- Build one identical
swapExactTokensForTokens(PancakeSwap V2) from each of two funded wallets, addressed to itself, signed but not yet sent. - Start a public-mempool watcher (for leak / first-seen timing).
- Fire both raw txs at the same instant — one via the public RPC
(
eth_sendRawTransaction), one via the blink endpoint (alsoeth_sendRawTransaction, just a private endpoint). - Wait for both to be mined.
- For each, read the pool's
Swapevents in the inclusion block, ordered by log index, to classify front-run (same-direction swap before ours by another sender), back-run (opposite-direction after ours), and sandwich (same address on both sides); reconstruct reserves to size the dislocation our swap created; and check whether the blink tx ever appeared in the public mempool. - Wait for the arb-back (
WAIT_FOR_REVERT): poll the V2 price against the live V3 reference until the gap our swap opened closes (or a timeout). The elapsed time is effectively the backrun latency, recorded per run, and keeps iterations independent. - Unwind (
UNWIND): sell the received TOKEN_OUT back to TOKEN_IN so capital recycles (you only need ~one trade of inventory per wallet, not N), then wait for that to settle.
Over many iterations it alternates which wallet uses which path (ALTERNATE_PATHS)
so per-wallet and in-block ordering effects cancel out.
npm install
cp .env.example .env # then edit .envFill in .env:
PUBLIC_RPC_HTTP/PUBLIC_RPC_WS— a BSC node you trust (WS needed for mempool/leak timing).BLINK_ENDPOINT(+ optionalBLINK_AUTH_HEADER/BLINK_AUTH_VALUE) — the blink private RPC.WALLET_A_PK,WALLET_B_PK— two funded sender wallets (secrets, never commit).- swap params (
TOKEN_IN/TOKEN_OUT/AMOUNT_IN/…) — default is sell 0.05 WBNB → USDT.
Check balances and approve the router:
npm run prepare:wallets # report only
APPROVE=true npm run prepare:wallets # also send approvalsDry run first (no broadcast — validates RPC, pair, quote, and the plan):
npm run experiment # DRY_RUN=true by defaultLive run (spends real funds, sends real swaps on mainnet):
DRY_RUN=false ITERATIONS=20 npm run experimentResults print a per-iteration comparison and an aggregate table, and are written to
results/run-<timestamp>.json.
- Backrun opportunity is a first-order, fee-free estimate: the optimal single-pool
arb profit (in
TOKEN_OUT) to push the pool back to a reference price, using reserves reconstructed to the point just after our swap. Reference price defaults to the pre-trade pool mid; setREF_PRICE(TOKEN_OUT per TOKEN_IN) to use an external mark (e.g. a CEX/Binance price) for a truer opportunity figure. It ignores the 0.25% LP fee and gas, so treat it as an upper bound on extractable value, separate from the realized backrun we detect on-chain. - Same-block interaction: if both wallets land in the same block they affect each
other's price;
ALTERNATE_PATHScancels the bias only in aggregate.WAIT_FOR_REVERTUNWINDreset the pool to ~the reference price before each new iteration, so cumulative drift is eliminated.UNWIND_VENUE=v3(recommended) unwinds on the deep V3 pool — near-zero impact, leaves the measured V2 pool untouched, no second revert-wait;v2round-trips the same thin pool (and re-dislocates it).
- If the arb never comes: when the opportunity is too small for bots,
WAIT_FOR_REVERTtimes out and recordsreverted: false(itself a finding); the unwind then helps reset. - Reserve reads at
block-1need a node that serves recent historical state (any full node does for recent blocks; a public dataseed may prune older ones). - Mempool/leak detection depends on the WS node supporting
newPendingTransactions; withoutPUBLIC_RPC_WSthe leak/first-seen columns are blank. - Only PancakeSwap V2 single-hop swaps are modeled. Multi-hop or V3 would need the swap builder and the pool-event decoder extended.
- Honest framing: blink should win on frontrun avoidance and leak=no; whether it wins on inclusion speed is exactly the open question this harness measures — don't assume it.
| File | Role |
|---|---|
src/config.ts |
env parsing + validation |
src/clients.ts |
viem public/WS/wallet clients |
src/swap.ts |
quote, gas, build + sign the swap |
src/senders.ts |
pluggable public / blink eth_sendRawTransaction senders |
src/monitor.ts |
public mempool watch + inclusion wait |
src/analyze.ts |
front/back-run detection, price impact, backrun sizing, V2 price |
src/refprice.ts |
deepest-V3-pool reference price auto-fetch |
src/reversion.ts |
wait for the pool to be arbed back (backrun latency) |
src/unwind.ts |
sell TOKEN_OUT back to TOKEN_IN (recycle capital) |
src/experiment.ts |
orchestrator + reporting (entry point) |
src/prepare.ts |
balances + router approvals (both tokens) |
src/refcheck.ts |
npm run ref:check — print V3 ref, V2 mid, and the gap |