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

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:
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.
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
);
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!

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

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

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.