A bundle hash is an acknowledgement, not a receipt. A successful simulation is a result for a chosen state and execution environment, not a reservation in the next block. Keeping those two distinctions in your logs makes a missing bundle much easier to investigate.

The Flashbots troubleshooting guide identifies several common causes: a transaction reverts, the incentive is insufficient, a competing bundle changes the opportunity, delivery is late, or the block producer does not use the relevant builder path. More than one can apply to the same attempt.

Work through the pipeline in order. Raising your bid will not fix invalid calldata or a bundle that never reached the endpoint.

Bundle diagnosis separates request acceptance, exact-state simulation, builder selection and canonical transaction receipts; success at one stage does not guarantee the next

First identify which API you used

The Flashbots RPC reference documents distinct formats:

Classic bundles MEV-Share bundles
Submission method eth_sendBundle mev_sendBundle
Transaction representation Ordered array of raw signed transactions Body items can contain signed transactions, transaction-hash references or nested bundles
Inclusion target One blockNumber inclusion.block and optional inclusion.maxBlock
Revert policy revertingTxHashes Per-transaction canRevert
Builder sharing builders privacy.builders
Simulation method eth_callBundle mev_simBundle

Do not copy fields or response handling between the two APIs. A MEV-Share hash reference also does not give your bot the signed bytes of the referenced transaction. Simulation may need the matching transaction to become available through the service’s supported workflow.

The current RPC documentation limits a MEV-Share bundle to one backrun transaction. Classic bundles have separately documented request-size and transaction-count limits. Check the live documentation before changing the shape of a request; an old SDK example may describe a different format.

1. Was the request accepted?

Retain the exact serialized request, response, HTTP status, target block, authentication address and local send/receive timestamps. Do not write signing private keys into diagnostic logs. Treat raw signed transaction bytes as sensitive too: possession can be enough to rebroadcast them.

For authentication, follow the reference’s X-Flashbots-Signature construction. Its ethers example signs id(body) with signMessage, where body is the exact JSON string sent. Changing whitespace or serializing a different object after signing changes the body hash. The authentication key identifies requests; it need not be the funded transaction signer.

An HTTP 200 response can still contain a JSON-RPC error. Check the JSON body before recording an accepted submission. Keep authentication failures, malformed parameters, rate limits and simulation failures as separate outcomes. Do not retry all of them with the same policy.

A returned bundle hash means the submission endpoint accepted that request under its API. It does not prove that every selected builder received it, that the bundle was competitive, or that a proposer selected a block containing it.

2. Did you simulate the same bytes and environment?

For classic eth_callBundle, distinguish three inputs:

When targeting block N+1, a typical test starts from block N’s state and uses N+1 as the execution block number. Keep the parent block’s hash with the report so you can detect a reorg. A bare latest state reference changes as the chain advances.

Wall-clock time is not automatically the correct target block timestamp. Likewise, separate calls to eth_call do not simulate an ordered bundle: transaction two may depend on state changes made by transaction one. Use the bundle simulator for the exact signed sequence.

Retain the result for the exact bytes later submitted. If the signer changes a nonce, gas cap, fee or calldata after simulation, you tested a different transaction.

3. Read the right simulation response

Classic eth_callBundle returns fields including results, totalGasUsed, coinbaseDiff and bundleGasPrice. Inspect transaction-level errors or reverts in results as well as any top-level RPC error. It is not the MEV-Share response shape: do not require a classic response to contain success: true or mevGasPrice.

mev_simBundle has its own result fields, including success, mevGasPrice and profit. The meaning of a field named profit is tied to that API’s accounting. It is not a substitute for checking your executor’s final balances and the expenses paid by your sender.

The following offline inspector reads a saved classic simulation response. It does not submit, simulate remotely or need a private key. Save it as inspect-bundle.mjs and run it with Node.js 22 or newer.

import { readFile } from "node:fs/promises";

const [responseFile, allowedFile] = process.argv.slice(2);

if (!responseFile) {
  throw new Error(
    "Usage: node inspect-bundle.mjs simulation.json [allowed-reverts.json]"
  );
}

const envelope = JSON.parse(await readFile(responseFile, "utf8"));

if (envelope.error) {
  throw new Error(`RPC error: ${JSON.stringify(envelope.error)}`);
}

// Accept either the full JSON-RPC response or its saved result object.
const result = envelope.result ?? envelope;

if (!Array.isArray(result.results) || result.results.length === 0) {
  throw new Error(
    "Expected a classic eth_callBundle result with transaction results"
  );
}

const allowedHashes = allowedFile
  ? JSON.parse(await readFile(allowedFile, "utf8"))
  : [];

if (
  !Array.isArray(allowedHashes) ||
  allowedHashes.some((h) => !/^0x[0-9a-fA-F]{64}$/.test(h))
) {
  throw new Error(
    "Allowed-reverts file must be an array of transaction hashes"
  );
}

const allowed = new Set(allowedHashes.map((h) => h.toLowerCase()));

const gas = BigInt(result.totalGasUsed);
const coinbaseDiff = BigInt(result.coinbaseDiff);

if (gas <= 0n) {
  throw new Error("Expected positive totalGasUsed");
}

const failures = result.results.flatMap((tx, index) => {
  if (tx.error == null && tx.revert == null) {
    return [];
  }

  return [
    {
      index,
      txHash: tx.txHash,
      error: tx.error ?? null,
      revert: tx.revert ?? null,
      // Caller intent does not prove a builder accepted this revert.
      markedAllowedByCaller: allowed.has(tx.txHash?.toLowerCase()),
    },
  ];
});

console.log(
  JSON.stringify(
    {
      stateBlockNumber: result.stateBlockNumber,
      totalGasUsed: gas.toString(),
      coinbaseDiffWei: coinbaseDiff.toString(),
      coinbaseWeiPerGasTruncated: (coinbaseDiff / gas).toString(),
      reportedBundleGasPrice: result.bundleGasPrice ?? null,
      failures,
      failuresNotMarkedAllowed: failures.filter(
        (failure) => !failure.markedAllowedByCaller
      ).length,
    },
    null,
    2
  )
);

Run it against the response you saved:

node inspect-bundle.mjs simulation.json

The optional allowed-reverts.json contains the hash array you intentionally submitted as revertingTxHashes. The inspector reports that intent; it does not prove that a builder accepted it, or that an error is an allowable EVM revert rather than an invalid transaction. No reported failures means only that this response did not report transaction-level failures, not that the bundle will land or that it earns your strategy’s minimum profit.

4. Is the revert actually safe to allow?

A reverted EVM call can still consume gas and the sender’s nonce when its transaction is included. Allowing that revert is an execution-policy decision, not a repair for an invalid signature, nonce gap or insufficient balance.

Suppose the first transaction approves a token and the second performs the arbitrage. Allowing the arbitrage to revert can leave you with an approval, fees and no revenue. If another transaction in the bundle pays a builder independently, inspect whether that payment can still occur when the profitable step fails.

Keep the revert policy as narrow as the strategy requires. Never add every failing transaction to the allow-list just to turn a red simulation report green. For MEV-Share, use its documented canRevert semantics rather than assuming they are identical to every classic builder’s handling.

5. What changed before your bundle’s position?

A top-of-block simulation can succeed while the same bundle fails after another transaction. Common conflicts include:

A nonce conflict requires the same sender and nonce. Two unrelated searchers using different accounts do not inherently share their profit-taking transaction nonce. They can still compete for the same opportunity or include the same target transaction.

Inspect the actual target block and your signed transactions. If another hash consumed your sender’s nonce, record that conflict. If not, compare the relevant state transitions before the position your bundle would have occupied. Flashbots’ troubleshooting documentation describes replaying competing bundle prefixes to identify conflicts; do not assume the cause from gas prices alone.

6. Did the builder earn enough to prefer it?

A useful diagnostic ratio is the simulated fee-recipient balance change divided by the bundle’s gas use. It is not a universal builder sorting algorithm. Builders optimize complete blocks under state conflicts, gas constraints and their own policies.

For an EIP-1559 transaction, the effective tip per gas is bounded by both the tip cap and maxFeePerGas - baseFeePerGas. A fee cap below the target base fee is an inclusion problem. The base fee is burned, so a high total gas price does not all become builder revenue.

Do not sum maxPriorityFeePerGas caps and call the result the paid incentive. Actual effective tips are paid per unit of gas used; explicit transfers to the fee recipient can contribute separately. Also avoid counting fees twice when you already use a simulator’s aggregate coinbaseDiff.

Compare against the block’s opportunities and the builder’s documented metrics, but keep the limits of the observation clear. Public block data does not reveal every losing bundle. Your own trading profit and the value offered to the builder are different numbers.

7. Did it arrive in time, and reach a relevant builder?

There is no universal “two seconds before the slot” cutoff that safely describes every builder and relay. Record submission start, acknowledgement and any available simulation or builder-receipt timestamps. Synchronize clocks when comparing measurements across machines.

An acknowledgement arrives after a network exchange; it does not identify the instant a builder last considered your bundle. Retrying after the target block is already built will not reopen it. For a new target, recheck the parent state, fees and strategy conditions instead of blindly recycling stale bytes.

Builder coverage matters, but the execution block’s miner/fee-recipient address is not a reliable builder identity registry. Use published relay delivery data and builder-specific telemetry where available. Do not infer coverage from one hardcoded coinbase address.

Classic multiplexing uses builders; MEV-Share uses privacy.builders. Select current names from the builder registrations, and understand the privacy tradeoff: sharing with more builders exposes the signed order flow to more recipients. More coverage is not an inclusion guarantee.

Replacement is not a multi-block validity window

For classic bundles, replacementUuid identifies a replaceable/cancellable submission. It does not turn one blockNumber into a range of blocks. Target another block with a new submission under the service’s current replacement semantics.

The cancellation documentation also warns about bids already placed. A cancellation request is not a way to undo a canonical transaction or guarantee that every previously produced block candidate has been withdrawn.

MEV-Share’s inclusion.maxBlock expresses a validity range. That is a different function from replacing a request. Track expiry and cancellation separately in your bot’s state machine.

The record that makes the next failure explainable

For each attempt, retain the ordered transaction hashes, exact signed bytes in protected storage, target environment, simulation result, revert policy, selected builders, request acknowledgement and timestamps. Afterward, check the individual transaction receipts and their canonical block hashes. Bundles do not have a standard Ethereum transaction receipt of their own.

BLAZED.sh can keep state reads and simulation RPC traffic local to an Ethereum mainnet node. External builder delivery remains a separate network path. Measure it separately using the bot RPC benchmarking guide.

If the missing input was a pending transaction rather than your submitted bundle, use the mempool diagnosis guide. For upcoming changes to commitment and payload timing, see ePBS and EIP-7732. None of those infrastructure changes removes the need to distinguish request acceptance, successful execution and actual inclusion.