Introducing RabbitStream⚡, earliest transaction detection from Solana Shreds with gRPC style filtering. Explore Now
shyft logo
Get API Key

Blogs

Dev Guides

Launching liquidity pools on Raydium with safeguarding strategy to counter bot manipulation (Part-2)

Shyft Logo

Team Shyft

· January 22, 2026

A step-by-step guide to launch token, maximize token’s visibility and trading volume using Jito bundles on Raydium

Liquidity Pool cover

In the previous article, we acquired essential data about the OpenBook market and token Account information. Now, we’ll leverage this information to construct the complete pool keys object, which is crucial for subsequent interactions with the liquidity pool.

Incase you missed out the first part of this blog, click here to check it out. The entire project code is available for you to follow along on GitHub here.

Since we have already got our wallet’s assigned account for the base token, now we go on to retrieve the quote mint information.

  //lpCreate.ts

  const accountInfo_quote = await connection.getAccountInfo(quoteMint);
  if (!accountInfo_quote) throw Error("no accountInfo_quote");
  const quoteTokenProgramId = accountInfo_quote.owner;
  const quoteDecimals = unpackMint(
    quoteMint,
    accountInfo_quote,
    quoteTokenProgramId
  ).decimals; //decimals for quote

  const associatedPoolKeys = await Liquidity.getAssociatedPoolKeys({
    version: 4,
    marketVersion: 3,
    baseMint,
    quoteMint,
    baseDecimals,
    quoteDecimals,
    marketId: new PublicKey(marketId),
    programId: MAINNET_PROGRAM_ID.AmmV4,
    marketProgramId: MAINNET_PROGRAM_ID.OPENBOOK_MARKET,
  });
  const { id: ammId, lpMint } = associatedPoolKeys;
  console.log("AMM ID: ", ammId.toString());
  console.log("lpMint: ", lpMint.toString());

Next, we determine the decimal precision for both the base and quote tokens. Subsequently, we use the getAssociatedPoolKeys function to generate a LiquidityAssociatedPoolKeysV4 object. This object is essentially a standard pool keys structure with certain OpenBook Market-specific fields omitted, which we’ve already acquired in the previous article. By merging these components, we construct a complete pool keys object, paving the way for subsequent operations.

  console.log("Quote Decimals: ", quoteDecimals);
  const targetPoolInfo = {
    id: associatedPoolKeys.id.toString(),
    baseMint: associatedPoolKeys.baseMint.toString(),
    quoteMint: associatedPoolKeys.quoteMint.toString(),
    lpMint: associatedPoolKeys.lpMint.toString(),
    baseDecimals: associatedPoolKeys.baseDecimals,
    quoteDecimals: associatedPoolKeys.quoteDecimals,
    lpDecimals: associatedPoolKeys.lpDecimals,
    version: 4,
    programId: associatedPoolKeys.programId.toString(),
    authority: associatedPoolKeys.authority.toString(),
    openOrders: associatedPoolKeys.openOrders.toString(),
    targetOrders: associatedPoolKeys.targetOrders.toString(),
    baseVault: associatedPoolKeys.baseVault.toString(),
    quoteVault: associatedPoolKeys.quoteVault.toString(),
    withdrawQueue: associatedPoolKeys.withdrawQueue.toString(),
    lpVault: associatedPoolKeys.lpVault.toString(),
    marketVersion: 3,
    marketProgramId: associatedPoolKeys.marketProgramId.toString(),
    marketId: associatedPoolKeys.marketId.toString(),
    marketAuthority: associatedPoolKeys.marketAuthority.toString(),
    marketBaseVault: marketBaseVault.toString(),
    marketQuoteVault: marketQuoteVault.toString(),
    marketBids: marketBids.toString(),
    marketAsks: marketAsks.toString(),
    marketEventQueue: marketEventQueue.toString(),
    lookupTableAccount: PublicKey.default.toString(),
  };
  console.log(targetPoolInfo);

  const poolKeys = jsonInfo2PoolKeys(targetPoolInfo) as LiquidityPoolKeys;

  // create liquidity pool and get pool keys + pool creation instructions

  const { innerTransactions } =
    await Liquidity.makeCreatePoolV4InstructionV2Simple({
      connection,
      programId: MAINNET_PROGRAM_ID.AmmV4,
      marketInfo: {
        programId: MAINNET_PROGRAM_ID.OPENBOOK_MARKET,
        marketId: marketId,
      },
      associatedOnly: false,
      ownerInfo: {
        feePayer: wallet.publicKey,
        wallet: wallet.publicKey,
        tokenAccounts: tokenAccountInfo,
        useSOLBalance: true,
      },
      baseMintInfo: {
        mint: baseMint,
        decimals: baseDecimals,
      },
      quoteMintInfo: {
        mint: quoteMint,
        decimals: quoteDecimals,
      },

      startTime: new BN(Math.floor(Date.now() / 1000)),
      baseAmount: new BN(baseAmount.toString()),
      quoteAmount: new BN(quoteAmount.toString()),

      computeBudgetConfig: await getComputeBudgetConfig(),
      checkCreateATAOwner: true,
      makeTxVersion: TxVersion.V0,
      lookupTableCache: LOOKUP_TABLE_CACHE,
      feeDestinationId: new PublicKey(
        "7YttLkHDoNj9wyDur5pM1ejNaAvT9X4eqaYcHQqtj2G5"
      ),
    });

  const message = new TransactionMessage({
    instructions: innerTransactions.flatMap((it) => it.instructions),
    payerKey: wallet.publicKey,
    recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
  }).compileToV0Message();

  const transaction = new VersionedTransaction(message);
  await transaction.sign([wallet]);

  return { poolKeys, createPoolTx: transaction };
}

We then go on to combine all the derived pool key information into one complete structure. This data is then processed into a format compatible with the SDK using the jsonInfo2PoolKeys function. Subsequently, the makeCreatePoolV4InstructionV2Simple function is then used to generate a preliminary transaction. From this transaction, we extract the necessary instructions, package them into a VersionedTransaction, and integrate them with the pool keys for subsequent Jito bundle creation.

We can launch the liquidity pool instantly by setting the startTime field to be the current time, or we can also set it to a timestamp later in case of launching the pool at a later time. To convert SOL field to wrapped SOL automatically, we can set the useSolBalance field to true.

With these to objects ready, let’s go back to the index.ts file:

  const createPoolIxResponse = await createPoolIx(
    new PublicKey(marketId),
    wallet,
    walletTokenAccounts,
    inputToken.mint,
    outputToken.mint,
    createLpBaseAmount,
    createLpQuoteAmount
   );
  if (createPoolIxResponse) {
    const { poolKeys, createPoolTx } = createPoolIxResponse;
    //extract instructions from createPoolIxResponse

    console.log(poolKeys);
    // we have the create pool instructions, now we add swap transactions
    // pass pool keys to lookup table
    const onlyPublicKeys = Object.values(poolKeys).filter(
      (poolKey) => poolKey instanceof PublicKey
    );
    const lookupTableAddress = await createLookupTable(
      wallet,
      onlyPublicKeys as PublicKey[]
    );

Since we already have the derived pool keys, we construct an address lookup table to optimize the subsequent swap transaction. This lookup table can be easily created by selecting all the pubkey objects from the full set of pool keys. The following function is used to create the lookup table:

import {
  ComputeBudgetProgram,
  AddressLookupTableProgram,
  TransactionMessage,
  VersionedTransaction,
  PublicKey,
  Keypair,
} from "@solana/web3.js";
import { connection } from "./config";
export default async function createLookupTable(
  wallet: Keypair,
  addresses: PublicKey[]
) {
  let latestBH = await connection.getLatestBlockhash("finalized");
  const recentSlot = await connection.getSlot("finalized");

  const bribe = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 25000,
  });
  const [lookupTableInst, lookupTableAddress] =
    await AddressLookupTableProgram.createLookupTable({
      authority: wallet.publicKey,
      recentSlot,
      payer: wallet.publicKey,
    });

  const LUTmessage = new TransactionMessage({
    instructions: [bribe, lookupTableInst],
    payerKey: wallet.publicKey,
    recentBlockhash: latestBH.blockhash,
  }).compileToV0Message();
  const tx = new VersionedTransaction(LUTmessage);
  tx.sign([wallet]);
  const lutSignature = await connection.sendRawTransaction(tx.serialize(), {
    maxRetries: 20,
  });
  console.log("luttxid:", lutSignature);
  await connection.confirmTransaction({
    blockhash: latestBH.blockhash,
    signature: lutSignature,
    lastValidBlockHeight: latestBH.lastValidBlockHeight,
  });
  await new Promise((resolve) => setTimeout(resolve, 5000));

  const extendInst = AddressLookupTableProgram.extendLookupTable({
    addresses: [wallet.publicKey, ...addresses],
    authority: wallet.publicKey,
    payer: wallet.publicKey,
    lookupTable: lookupTableAddress,
  });

  // -------- step 1.7: extend lookup table --------
  const ExtendMessage = new TransactionMessage({
    instructions: [bribe, extendInst],
    payerKey: wallet.publicKey,
    recentBlockhash: latestBH.blockhash,
  }).compileToV0Message();
  const extendTx = new VersionedTransaction(ExtendMessage);
  extendTx.sign([wallet]);
  const extendSignature = await connection.sendRawTransaction(
    extendTx.serialize(),
    { maxRetries: 20 }
  );
  console.log("extendtxxid:", extendSignature);
  await connection.confirmTransaction({
    blockhash: (await connection.getLatestBlockhash()).blockhash,
    signature: extendSignature,
    lastValidBlockHeight: (
      await connection.getLatestBlockhash()
    ).lastValidBlockHeight,
  });
  // wait for tx to finalize
  await new Promise((resolve) => setTimeout(resolve, 10000));
  // return address lookup table
  return lookupTableAddress;
}

To summarize, these are the steps required to create a lookup table:

  • Obtain a recent blockhash.
  • Pay a compute budget fee to speed up transaction processing. (an additional fee to bribe the validator)
  • Initialize the lookup table, and populating the lookup table with necessary addresses, including the wallet and pool-related accounts.
  • Create and submit a versioned transaction containing the lookup table creation and population instructions.

Please ensure that there are sufficient promise based delays between these transactions are finalized in order, without failing. This would ensure the lookup table is created and the versioned transaction is submitted.

Creating a swap transaction

Due to the low initial liquidity and the bundled transaction structure we will be performing a fixed input swap with a wider slippage tolerance to accommodate potential price fluctuations.

We now proceed to create the swap transaction,

// create swap instructions in src/swapCreate.ts

import {
  InnerSimpleV0Transaction,
  Liquidity,
  Percent,
  TxVersion,
} from "@raydium-io/raydium-sdk";
import {
  TransactionInstruction,
  TransactionMessage,
  VersionedTransaction,
  PublicKey,
} from "@solana/web3.js";
import { connection } from "./config";

export default async function createSwapIx(
  poolKeys: any,
  inputTokenAmount: any,
  outputToken: any,
  walletTokenAccounts: any,
  wallet: any,
  times: number = 1,
  LUT: PublicKey
) {
  // -- get lookup table
  const lookupTableAcc = await connection.getAddressLookupTable(LUT);
  // -------- step 1: coumpute amount out --------
  const { minAmountOut } = Liquidity.computeAmountOut({
    poolKeys: poolKeys,
    poolInfo: await Liquidity.fetchInfo({ connection, poolKeys }),
    amountIn: inputTokenAmount,
    currencyOut: outputToken,
    slippage: new Percent(30, 100), // for new LP its quite common to have high slippage
  });
  const ix = await Liquidity.makeSwapFixedInInstruction(
    {
      poolKeys,
      userKeys: {
        owner: wallet.publicKey,
        tokenAccountIn: walletTokenAccounts[0],
        tokenAccountOut: walletTokenAccounts[1],
      },
      amountIn: inputTokenAmount,
      minAmountOut: minAmountOut.raw,
    },
    4
  );
  // repeat the ix times amount in the array
  let ixs = [];
  for (let i = 0; i < times; i++) {
    ixs.push(...ix.innerTransaction.instructions);
  }
  // create a transaction that duplicates swap ix in the same tx
  const message = new TransactionMessage({
    instructions: ixs,
    payerKey: wallet.publicKey,
    recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
  }).compileToV0Message([lookupTableAcc.value!]);

  const transaction = new VersionedTransaction(message);
  await transaction.sign([wallet]);

  return transaction;
}

This function basically fetches the lookup table created in the previous step, and calculates the worst possible number of tokens we will get once our swap transaction goes through. We utilize the Raydium SDK to generate a fixed-input swap instruction. To optimize transaction size and accommodate multiple swaps, we extract these instructions and create a new transaction that references addresses using the previously created lookup table. This replaces each address (32 bytes) with a 1 byte reference, effectively saving us 31 * (total amount of addresses) bytes of space (from the 1232 byte) limit so that we can stack multiple swaps in 1 transaction, so that we can furthermore stack more swaps into a single bundle.

We can then call this newly created function in our main function,

//main.js

const swapIx = await createSwapIx(
   poolKeys,
   inputTokenAmount,
   outputToken,
   walletTokenAccounts,
   wallet,
   3,
   lookupTableAddress
);

Adding Transactions to the Jito Bundle

After figuring out the optimized swap transactions, and stacking them in the the same bundle using the lookup table, we move forward to execute them using the Jito Bundle.

Jito bundles are a mechanism for grouping multiple transactions into a single, atomic operation. This is particularly useful for complex interactions like creating a liquidity pool and executing subsequent trades. By using a Jito bundle, we can ensure that all transactions within the bundle are either fully executed or reverted if any part fails.

A single Jito bundle can hold up to 5 transactions. By using address lookup tables, we can fit about 5 swap instructions into a single transaction. To ensure timely processing and avoid potential front-running, it’s crucial to include a small tip for the Jito validators as the final transaction in the bundle.

The function below illustrates submitting transactions using Jito,

import { InnerSimpleV0Transaction } from "@raydium-io/raydium-sdk";
import { Bundle } from "jito-ts/dist/sdk/block-engine/types";
import { PublicKey, VersionedTransaction, Signer } from "@solana/web3.js";
import { connection, sc, wallet } from "./config";
// create a Jito bundle object, add the tx, monitor it
export default async function submitJitoBundle(
  txs: VersionedTransaction[],
  payer: PublicKey,
  signer: Signer,
  LUT: PublicKey
) {
  // same LUT can be used for both create tx and LUT
  const recentBlockHash = (await connection.getLatestBlockhash()).blockhash;

  let bundle = new Bundle(txs, 5);

  //get a tip account
  const tipAcc = await sc.getTipAccounts();
  const maybebundle = bundle.addTipTx(
    wallet,
    25000,
    new PublicKey(tipAcc[0]),
    recentBlockHash
  );
  if (maybebundle instanceof Error) {
    throw new Error("bundle error");
  } else {
    bundle = maybebundle;
  }

  const bundleId = await sc.sendBundle(bundle);
  // search the ID on the jito website
  console.log(`bundleId: ${bundleId}`);
  return bundleId;
}

The addTipTx function either generates an updated bundle or flags an error based on the outcome of the on-chain simulation. Successful bundle execution requires all internal transactions to complete successfully and the tip account to be valid. Our getTipAccounts helper simplifies the process of identifying a suitable tip recipient.

once this function is defined, we simply call it in the main function, with the version transaction objects, with the tip at the last.

// pass pool creation ix, swap ix, lookup table address to jito bundle

    const submitBundleRes = await submitJitoBundle(
      // create LP tx, swap tx, swap tx, swap tx (each 5 swap instructions)
      // 1 tip tx added in the submitJitoBundle function
      [createPoolTx, swapIx, swapIx, swapIx],
      wallet.publicKey,
      wallet,
      lookupTableAddress
    );
    console.log("submitBundleRes:", submitBundleRes);
  } else {
    console.log("createPoolIx failed");
    return;
  }
}

main()
  .then((value) => console.log(value))
  .catch((err) => console.log(err));

Once successfully executed, it returns a Jito bundle ID, which can be used for monitoring purposes. You can also inspect bundles on the Jito website using the generated bundle ID, using their block engine explorer.

That’s everything about this article series which shows how to launch your tokens successfully on Raydium. In case you missed the previous part, here is the link: Link to the first part

The entire project code is available here on GitHub, feel free to clone it and give it a spin!

Resources

Low-latency Streaming
Raydium
Solana Blockchain
Solana Development

Related Posts

How to reconnect and replay slots with Solana Yellowstone gRPC
Shyft

How to reconnect and replay slots with Solana Yellowstone gRPC

In this article you will learn how to implement a reconnect logic for your Solana gRPC streams with replay functionality...

January 24, 2026

How to modify Solana Yellowstone gRPC subscribe requests without disconnecting
Shyft

How to modify Solana Yellowstone gRPC subscribe requests without disconnecting

Learn how to modify your yellowstone gRPC Subscribe Requests on Solana without stopping your stream or losing data ...

January 24, 2026

Real-Time Solana Data Streaming with gRPC: Accounts, Transactions, Blocks
Shyft

Real-Time Solana Data Streaming with gRPC: Accounts, Transactions, Blocks

A comprehensive guide on how to stream Transactions, Accounts, and Block updates swiftly using Shyft’s gRPC Services ...

January 22, 2026

Get in touch with our discord community and keep up with the latest feature
releases. Get help from our developers who are always here to help you take off.

GithubTwitterLinked inDiscordTelegramBlogsBlogs

Products

RabbitStreamgRPC NetworkSuperIndexerSolana APIs
Contact Us|Email: genesis@shyft.to