Full Example

// node LTS (v18.12.1)
// consider node-fetch to replace native fetch for lower

const https = require("https");
const { ethers } = require('ethers');

const { utils } = ethers;

// Find me at: https://github.com/FastLane-Labs/auction/blob/main/contracts/abis/FastLaneAuctionHandlerAbi.json
const abi = require('./FastLaneAuctionHandlerAbi.json');

const RELAY = "https://beta-rpc.fastlane-labs.xyz/";

const PRIVATE_KEY = "0xPrivateK3Y";

// Only used for Gas Price and Nonces
const ALCHEMY_KEY = "alchemy-apikey";
const provider = new ethers.providers.AlchemyProvider("matic", ALCHEMY_KEY);

const signer = new ethers.Wallet(PRIVATE_KEY);

console.log(`Address: ${signer.address}`);

const BID_AMOUNT = 100000;

// Bogus echo contract see https://github.com/FastLane-Labs/auction/blob/main/contracts/helpers/SearcherHelperRepayerEcho.sol
const searcherContract = "0x2FEf5C22a63CC8FE424bd83af9D1955A5c2d9e2E"; 

const auctionHandler = "0xf5DF545113DeE4DF10f8149090Aa737dDC05070a";

// Assuming as a searcher I intend to use _searcherCallDataBytes
// when received on my searcher contract `fastLaneCall(` callback
// to then trigger searcherContract.doMEV(uint256,string);
  
// See: Searcher Call Data section of the docs for more informations
// 0x2FEf5C22a63CC8FE424bd83af9D1955A5c2d9e2E contract will not use it, just an example
const searcherAbi = [
    `function doMEV(uint256, string)`,
  ];

const iface = new ethers.utils.Interface(searcherAbi);

// Grab bytes for doMEV(uint256, string)
const searcherCallDataBytes = iface.encodeFunctionData("doMEV", [ethers.utils.parseEther("1.0"), "unused"]);

const AuctionHandlerContract = new ethers.Contract(auctionHandler, abi, signer);


async function createBundle() {

    const nonce = await provider.getTransactionCount(signer.address);
    const feeData = await provider.getFeeData();

    const { gasPrice, lastBaseFeePerGas, maxFeePerGas, maxPriorityFeePerGas } = feeData;
    
    console.log(`GAS: ${utils.formatUnits(gasPrice, "gwei")} gwei, NONCE: ${nonce}`);
    console.log(`lastBaseFeePerGas: ${utils.formatUnits(maxFeePerGas, "gwei")} gwei`);
    console.log(`maxFeePerGas: ${utils.formatUnits(maxFeePerGas, "gwei")} gwei`);
    console.log(`maxPriorityFeePerGas: ${utils.formatUnits(maxPriorityFeePerGas, "gwei")} gwei`);

    // Forge our own oppTx to backrun for demo purposes
    // We'll sign this one from `signer` but normally it would originate from another EOA
    const oppTx = {
        type: 2,
        from: signer.address,
        to: signer.address,
        value: utils.parseEther('0.000001'),
        nonce: nonce,
        gasLimit: "21000",
        maxPriorityFeePerGas: gasPrice.add(lastBaseFeePerGas), 
        maxFeePerGas: gasPrice.add(gasPrice),
        chainId: 137,
    };

 
    const checkOpp = await signer.checkTransaction(oppTx);
    const signedOppTx = await signer.signTransaction(oppTx);

    const oppTxHash = ethers.utils.keccak256(signedOppTx);
    console.log("Precomputed oppTxHash:", oppTxHash);
    
    const searcherTx = await AuctionHandlerContract.populateTransaction.submitFlashBid(
        BID_AMOUNT, 
        oppTxHash, 
        searcherContract, 
        searcherCallDataBytes, 
        {
            type: 2,
            value: BID_AMOUNT,
            nonce: nonce + 1,
            gasLimit: utils.hexlify(500000),
            // Must be same gas than opp
            maxPriorityFeePerGas: gasPrice.add(lastBaseFeePerGas), 
            maxFeePerGas: gasPrice.add(gasPrice), 
        });

    searcherTx.chainId = 137;

    const checkSearcher = await signer.checkTransaction(searcherTx);
    const signedSearcherTx = await signer.signTransaction(searcherTx);

    console.log("Precomputed searcherTxHash:", ethers.utils.keccak256(signedSearcherTx));
    

    const bundle = [
        signedOppTx,
        signedSearcherTx
    ];
    // console.log(bundle);
    return bundle;
}




const agent = new https.Agent({
  keepAlive: true,
});

const ping = () => fetch(`${RELAY}/ping`, { agent }).then(r => console.log(`PONG : ${r.statusText}`));

const sendBundle = postData => fetch(RELAY, {
    method: "POST", // or 'PUT',
    agent,
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(postData),
});

const keepWarm = async() => {
    setInterval(ping, 1000*60*10);
    return ping();
}

async function main() {

    // Warm it up.
    await keepWarm();

    const pfl_bundle =  await createBundle(); // See submitFlashBid js doc

    const postData = {
    jsonrpc: "2.0",
    method: "pfl_addSearcherBundle",
    params: [pfl_bundle],
    id: 1,
    };

    console.log(postData);

  // Whenever needed. Fire a bundle.
  const start = +Date.now();
  await sendBundle(postData)
    // See https://fastlane-labs.gitbook.io/polygon-fastlane/reference/relay-json-rpc-api 
    // as response can still have a .error field
    .then(async(response) => {
      const elapsed = +Date.now() - start;
      console.log("Success:", response.statusText, elapsed);

      const json = await response.json();
      console.log(json);

      if (json.error) throw json.error.message;
    })
    .catch((error) => {
      console.error("Error:", error);
    });
};


const getAuthor =  async(blockNum) => {
  const response = await fetch(`https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, {
    method: 'POST',
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "id": 1,
      "jsonrpc": "2.0",
      "method": "bor_getAuthor",
      "params": [
           '0x'+blockNum.toString(16)
      ]
 }),
  });
  const json = await response.json();
  return json.result;
}

let waitBlock = 0;
provider.on('block', async(block) => {
  try {
    console.log(`Block: ${block}`);
    if (block < waitBlock) return;
    const author = await getAuthor(block);
    console.log(author);
    const validators = [
      '0x127685D6dD6683085Da4B6a041eFcef1681E5C9C',
      '0x02f70172f7f490653665c9bfac0666147c8af1f5',
      '0xd2e4bE547B21E99A668f81EEcF0298471A19808f',
      '0xb9EDE6f94D192073D8eaF85f8db677133d483249',
      '0x9eaD03F7136Fc6b4bDb0780B00a1c14aE5A8B6d0',
    ].map(x => x.toLowerCase());
    if (author && validators.includes(author.toLowerCase())) {
      console.log('Validator span');

      // Will shoot one demo bundle and close
      await main();
      console.log('Closing');
      process.exit(0);
    } else {
      waitBlock = block + 10;
    }
  } catch (err) {
    console.error(err);
  }

});

```

Nodejs Version

In this example we backrun a transfer to ourselves

Also don't forget to check Helpers.

Last updated