Skip to main content

33 min read

Overview

Jupiter, Solana's leading swap aggregator and routing protocol, is a powerful tool for developers looking to build trading tools, dApps, and other DeFi applications. In this guide, we will use Metis, Jupiter's onchain routing engine, via Quicknode's Metis add-on to create a simple Solana trading bot.


TL;DR
  • Metis is Jupiter's onchain routing engine, hosted as a managed endpoint via Quicknode's Metis add-on
  • Build a TypeScript trading bot that polls Jupiter's /quote endpoint and executes swaps when price thresholds are met
  • Use @jup-ag/api with @solana/kit to fetch swap instructions, build versioned transactions, and submit them directly to Solana
  • The bot tracks SOL/USDC balances, logs each trade to a JSON file, and updates its thresholds after every swap
Prefer a video walkthrough? Follow along with Sahil and learn how to create a trading bot on Solana using Jupiter API.
Subscribe to our YouTube channel for more videos!

Note: This guide is for educational purposes only. Quicknode does not provide financial advice or endorse any trading strategies. Always do your own research and consult with a financial advisor before making any investment decisions.

What You Will Do

In this guide, you will:


  1. Get an overview of Jupiter
  2. Learn how to use the Metis API via Quicknode's Metis add-on
  3. Create a class-based trading bot that swaps SOL for USDC using Jupiter's API. The bot will use the Jupiter API to monitor the market for specific conditions and execute Jupiter trades when met.

What You Will Need

This guide assumes you have a basic understanding of building TypeScript dApps, DeFi, and working with Solana wallets.

You will also need:


This guide uses the following dependencies:

DependencyVersion
@jup-ag/api^6.0.48
@solana/kit^6.9.0
@solana-program/token^0.13.0
@solana-program/address-lookup-table^0.11.0

Enhanced Performance with Quicknode

Reliable and high-performance blockchain infrastructure is critical to get the most out of your Solana trading bot. Quicknode provides fast and scalable Solana RPC node endpoints that significantly enhance your bot's responsiveness and efficiency. Sign Up for an account and get started with Quicknode today.

In addition to a solid RPC endpoint, consider leveraging the Quicknode's Jupiter API plugin, Metis for optimized trade execution. This add-on hosts the Metis binary on Quicknode's infrastructure, giving you a private, managed endpoint for quote fetching and swap execution. Never worry about setting up or maintaining a server with our rolling restarts and low latency. Markets and pools show up immediately.

This add-on will not use your account credits as it depends on its own Solana validator node, segmented from our pro network for maximum reliability.

Alternatively, you can access a Public endpoint for Jupiter's API here: https://public.jupiterapi.com/.

What is Jupiter?

Jupiter is a Web3 Swap program on Solana. Jupiter allows users to find efficient routes for swapping tokens on Solana. Token swapping is a core feature of DeFi that enables users to trade one token for another while accounting for the market value of each token.

Jup.ag Swap Source: jup.ag

Jupiter aggregates pricing from many decentralized exchanges (DEXs) and automated market makers (AMMs) and employs a unique algorithm called "smart routing" that allows users to find the best price for their swap.

Jupiter Routes Source: Jupiter Docs

Jupiter will also search for inefficiencies in intermediary swaps (e.g., USDC-mSOL-SOL instead of USDC-SOL) to find lower costs for users. When executing swaps, Jupiter also utilizes a concept called Trade Splitting, which breaks a trade into smaller trades across multiple DEXs to find the best price.

Using the Jupiter Metis API

The Metis API exposes Jupiter's onchain routing engine as an HTTP service for developers building trading tools, dApps, and other DeFi applications. It gives you access to Jupiter's smart routing algorithm, returning swap quotes and Solana transaction instructions. Quicknode's Metis add-on runs the Metis binary on its infrastructure, so you have a stable, managed endpoint. The API includes five main methods:

EndpointJS Method Name(s)TypeDescription
/quotequoteGet
quoteGetRaw
GETGet best-priced quote for a swap given two tokens and a swap amount
/swapswapPost
swapPostRaw
POSTReturns a Solana swap transaction from a quote
/swap-instructionsswapInstructionsPost
swapInstructionsPostRaw
POSTReturns Solana swap instructions from a quote
/program-id-to-labelprogramIdToLabelGet
programIdToLabelGetRaw
GETReturns a mapping of names/labels for all program ID
/indexed-route-mapindexedRouteMapGet
indexedRouteMapGetRaw
GETReturns a hash map, input mint as key and an array of valid output mint as values

Requests can be made with the following format: {server}/{endpoint}?{query/body}. Here's an example for getting a quote for a swap of 100 USDC to SOL with a cURL:

curl -L 'https://jupiter-swap-api.quiknode.pro/YOUR_ENDPOINT/quote?inputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&outputMint=So11111111111111111111111111111111111111112&amount=100000000' \
-H 'Accept: application/json'

Make sure to replace https://jupiter-swap-api.quiknode.pro/YOUR_ENDPOINT with our own Metis endpoint. Alternatively, you can use our public endpoint: https://public.jupiterapi.com (some methods may not be available). You can find your Metis address from your Quicknode Dashboard's add-on page (https://dashboard.quicknode.com/endpoints/YOUR_ENDPOINT/add-ons):

Metis Address

To use the Jupiter JS Client, you can install it via npm:

npm install @jup-ag/api

You will need to create an instance of the Jupiter API client and pass in your Metis key (e.g., https://jupiter-swap-api.quiknode.pro/YOUR_ENDPOINT) or a public endpoint available here :

import { createJupiterApiClient } from '@jup-ag/api';

const ENDPOINT = `https://jupiter-swap-api.quiknode.pro/XX123456`; // 👈 Replace with your Metis Key or a public one https://public.jupiterapi.com/
const CONFIG = {
basePath: ENDPOINT
};
const jupiterApi = createJupiterApiClient(CONFIG);

And then call the methods you need, for example:

jupiterApi.quoteGet({
inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
outputMint: "So11111111111111111111111111111111111111112",
amount: 100_000_000,
}).then((quote) => {
console.log(quote.outAmount, quote.outputMint);
}).catch((error) => {
console.error(error);
});

All methods and documentation can be found at Jupiter Docs:

Let's test it out by creating a simple trading bot that uses Jupiter's API to monitor the market for specific conditions and execute trades when they are met.

info

Jupiter also offers Swap V2, a unified API with a managed execution path (Meta-Aggregator) and a raw instruction path (Router). Learn more about How to Swap Tokens With Jupiter Ultra API.

Trading Bot


Before You Start

This example is for educational purposes. Do not use this code in a production environment. Trades executed on Solana's mainnet are irreversible and can result in financial loss. Always do your own research and consult with a financial advisor before making any investment decisions.

Here's what our bot is going to do:

  • The bot will expect a wallet with a SOL and USDC balance.
  • The bot will monitor the market (using Jupiter's get quote method on a specified interval).
  • The bot will execute a trade using Jupiter's swap method when the market price meets our defined conditions.
  • The bot will log our swap if it is successful and update the next trade conditions so the bot will execute its next swap at a predefined % change from the previous swap.
  • The bot will run until we terminate it or insufficient SOL is available to execute the next trade.

Set Up Your Project

First, let's create a new project directory:

mkdir jupiter-trading-bot
cd jupiter-trading-bot

Then, initialize a new Node.js project:

npm init -y

Next, install your dependencies. We will need the Jupiter API, Solana Kit, the SPL token program client, and the address lookup table program client:

npm install @jup-ag/api @solana/kit @solana-program/token @solana-program/address-lookup-table
npm install --save-dev @types/node

Create three files in your project directory: bot.ts, index.ts, and .env:

echo > bot.ts && echo > index.ts && echo > .env

Define .env Variables

Open the .env file and add the following variables:

.env
# Replace with your Solana wallet secret key
SECRET_KEY=[00, 00, ... 00]
# Replace with your Quicknode Solana Mainnet RPC endpoint
SOLANA_ENDPOINT=https://example.solana-mainnet.quiknode.pro/123456/
# Replace with your Quicknode Jupiter API endpoint (or a public one: https://public.jupiterapi.com/)
METIS_ENDPOINT=https://jupiter-swap-api.quiknode.pro/123456

Make sure to replace the variables with your own. Add .env to your .gitignore to avoid committing your secret key.

If you do not have a file system wallet, you can create one by running:

solana-keygen new --no-bip39-passphrase --silent --outfile ./my-keypair.json 

Import Dependencies

Open bot.ts and import the necessary dependencies:

bot.ts
import { createSolanaRpc, createSolanaRpcSubscriptions, address, Address, KeyPairSigner, createKeyPairSignerFromBytes, Instruction, AccountRole, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, compressTransactionMessageUsingAddressLookupTables, signTransactionMessageWithSigners, getSignatureFromTransaction, sendAndConfirmTransactionFactory, AddressesByLookupTableAddress } from "@solana/kit";
import { createJupiterApiClient, SwapApi, ResponseError, QuoteGetRequest, QuoteResponse, Instruction as JupiterInstruction, AccountMeta } from '@jup-ag/api';
import { findAssociatedTokenPda } from "@solana-program/token";
import { fetchAddressLookupTable } from "@solana-program/address-lookup-table";
import * as fs from 'fs';
import * as path from 'path';

const LAMPORTS_PER_SOL = 1_000_000_000n;

Define Interfaces

Let's create a couple of interfaces to define the bot's configuration and the trade conditions:

bot.ts (continued)
interface ArbBotConfig {
solanaEndpoint: string; // e.g., "https://ex-am-ple.solana-mainnet.quiknode.pro/123456/"
metisEndpoint: string; // e.g., "https://jupiter-swap-api.quiknode.pro/123456/"
secretKey: Uint8Array;
firstTradePrice: number; // e.g. 94 USDC/SOL
targetGainPercentage?: number;
checkInterval?: number;
initialInputToken: SwapToken;
initialInputAmount: number;
}

interface NextTrade extends QuoteGetRequest {
nextTradeThreshold: number;
}

export enum SwapToken {
SOL,
USDC
}

interface LogSwapArgs {
inputToken: string;
inAmount: string;
outputToken: string;
outAmount: string;
txId: string;
timestamp: string;
}

  • ArbBotConfig will be used to define the bot's configuration, including the Solana and Jupiter API endpoints, the secret key, the initial trade price, the target gain percentage, the check interval, and the initial input token and amount.
  • NextTrade will be used to define the next trade's conditions, including the input and output tokens, the amount, and the threshold.
  • LogSwapArgs will be used to log the details of each trade to a json file.

Define Bot Class

Let's frame an ArbBot class that will handle the bot's logic. We will predefine the class and its methods and then fill in the details in the next section. We will also populate a few helper methods so we can save time on those. Add the following to bot.ts:

bot.ts (continued)
export class ArbBot {
private rpc: ReturnType<typeof createSolanaRpc>;
private rpcSubscriptions: ReturnType<typeof createSolanaRpcSubscriptions>;
private sendAndConfirmTransaction!: ReturnType<typeof sendAndConfirmTransactionFactory>;
private jupiterApi: SwapApi;
private wallet!: KeyPairSigner;
private secretKey: Uint8Array;
private usdcMint: Address = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
private solMint: Address = address("So11111111111111111111111111111111111111112");
private usdcTokenAccount!: Address;
private solBalance: bigint = 0n;
private usdcBalance: number = 0;
private checkInterval: number = 1000 * 10;
private lastCheck: number = 0;
private priceWatchIntervalId?: NodeJS.Timeout;
private targetGainPercentage: number = 1;
private nextTrade: NextTrade;
private waitingForConfirmation: boolean = false;

constructor(config: ArbBotConfig) {
// TODO
}

async init(): Promise<void> {
this.wallet = await createKeyPairSignerFromBytes(this.secretKey);
this.sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc: this.rpc, rpcSubscriptions: this.rpcSubscriptions });
const [usdcTokenAccount] = await findAssociatedTokenPda({
mint: this.usdcMint,
owner: this.wallet.address,
tokenProgram: address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
});
this.usdcTokenAccount = usdcTokenAccount;
console.log(`🤖 Initiating arb bot for wallet: ${this.wallet.address}.`)
await this.refreshBalances();
console.log(`🏦 Current balances:\nSOL: ${Number(this.solBalance) / Number(LAMPORTS_PER_SOL)},\nUSDC: ${this.usdcBalance}`);
this.initiatePriceWatch();
}

private async refreshBalances(): Promise<void> {
// TODO
}


private initiatePriceWatch(): void {
// TODO
}

private async getQuote(quoteRequest: QuoteGetRequest): Promise<QuoteResponse> {
// TODO
}

private async evaluateQuoteAndSwap(quote: QuoteResponse): Promise<void> {
// TODO
}

private async executeSwap(route: QuoteResponse): Promise<void> {
// TODO
}

private async updateNextTrade(lastTrade: QuoteResponse): Promise<void> {
// TODO
}

private async logSwap(args: LogSwapArgs): Promise<void> {
const { inputToken, inAmount, outputToken, outAmount, txId, timestamp } = args;
const logEntry = {
inputToken,
inAmount,
outputToken,
outAmount,
txId,
timestamp,
};

const filePath = path.join(__dirname, 'trades.json');

try {
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, JSON.stringify([logEntry], null, 2), 'utf-8');
} else {
const data = fs.readFileSync(filePath, { encoding: 'utf-8' });
const trades = JSON.parse(data);
trades.push(logEntry);
fs.writeFileSync(filePath, JSON.stringify(trades, null, 2), 'utf-8');
}
console.log(`✅ Logged swap: ${inAmount} ${inputToken} -> ${outAmount} ${outputToken},\n TX: ${txId}`);
} catch (error) {
console.error('Error logging swap:', error);
}
}

private terminateSession(reason: string): void {
console.warn(`❌ Terminating bot...${reason}`);
console.log(`Current balances:\nSOL: ${Number(this.solBalance) / Number(LAMPORTS_PER_SOL)},\nUSDC: ${this.usdcBalance}`);
if (this.priceWatchIntervalId) {
clearInterval(this.priceWatchIntervalId);
this.priceWatchIntervalId = undefined; // Clear the reference to the interval
}
setTimeout(() => {
console.log('Bot has been terminated.');
process.exit(1);
}, 1000);
}

private jupiterInstructionToKitInstruction(
instruction: JupiterInstruction | undefined
): Instruction | null {
if (instruction === null || instruction === undefined) return null;
return {
programAddress: address(instruction.programId),
accounts: instruction.accounts.map((key: AccountMeta) => ({
address: address(key.pubkey),
role: key.isWritable && key.isSigner ? AccountRole.WRITABLE_SIGNER
: key.isSigner ? AccountRole.READONLY_SIGNER
: key.isWritable ? AccountRole.WRITABLE
: AccountRole.READONLY,
})),
data: new Uint8Array(Buffer.from(instruction.data, 'base64')),
};
};

private async getAddressLookupTableAccounts(
keys: string[]
): Promise<AddressesByLookupTableAddress> {
const result: AddressesByLookupTableAddress = {};
await Promise.all(
keys.map(async (key) => {
const altAccount = await fetchAddressLookupTable(this.rpc, address(key));
result[address(key)] = altAccount.data.addresses;
})
);
return result;
};

private async postTransactionProcessing(quote: QuoteResponse, txid: string): Promise<void> {
const { inputMint, inAmount, outputMint, outAmount } = quote;
await this.updateNextTrade(quote);
await this.refreshBalances();
await this.logSwap({ inputToken: inputMint, inAmount, outputToken: outputMint, outAmount, txId: txid, timestamp: new Date().toISOString() });
}
}

Before moving on, let's walk through what we have here:

  • First, we define a few class properties, including the Solana RPC client, the Jupiter API, the wallet signer, the USDC and SOL mint addresses, the USDC token account, the check interval, the last check, the price watch interval id, the target gain percentage, the next trade, and a flag to indicate if the bot is waiting for confirmation. We will use these to track the bot's state and manage the trading logic.
  • We define a constructor that takes a configuration object and initializes the bot's properties. We also define an init method that handles async setup (creating the wallet signer, deriving the token account) and starts the price watch interval.
  • We are defining a few helper methods:
    • logSwap will be used to log the details of each trade to a json file.
    • terminateSession will be used to terminate the bot and log the reason for termination.
    • jupiterInstructionToKitInstruction will convert a Jupiter instruction into a Kit IInstruction.
    • getAddressLookupTableAccounts will be used to fetch address lookup table accounts.
    • postTransactionProcessing will trigger necessary steps after a successful swap (updateNextTrade, refreshBalances, and logSwap). We will define those methods in the next section.

Constructor

Let's build our constructor to initiate an instance of ArbBot. We have already defined our ArbBotConfig interface, so we can use that to define the constructor's parameters. Add the following to bot.ts:

bot.ts (continued)
    constructor(config: ArbBotConfig) {
const {
solanaEndpoint,
metisEndpoint,
secretKey,
targetGainPercentage,
checkInterval,
initialInputToken,
initialInputAmount,
firstTradePrice
} = config;
this.rpc = createSolanaRpc(solanaEndpoint);
this.rpcSubscriptions = createSolanaRpcSubscriptions(solanaEndpoint.replace('https://', 'wss://'));
this.jupiterApi = createJupiterApiClient({ basePath: metisEndpoint });
this.secretKey = secretKey;
if (targetGainPercentage) { this.targetGainPercentage = targetGainPercentage }
if (checkInterval) { this.checkInterval = checkInterval }
this.nextTrade = {
inputMint: initialInputToken === SwapToken.SOL ? this.solMint : this.usdcMint,
outputMint: initialInputToken === SwapToken.SOL ? this.usdcMint : this.solMint,
amount: initialInputAmount,
slippageBps: 300,
nextTradeThreshold: firstTradePrice,
};
}

  • First, we destructure the configuration object and assign the properties to the class instance.
  • We then use the Solana endpoint to create a Solana RPC client and an RPC subscriptions client (the WebSocket equivalent, used for transaction confirmation).
  • We store the secret key for use in init(), where createKeyPairSignerFromBytes (async) creates the wallet signer and findAssociatedTokenPda derives the USDC token account.
  • We set the target gain percentage and check interval if they are provided (recall we have included default values for these in the class).
  • Finally, we set the next trade conditions based on the initial input token (and amount) and the first trade price. The direction of the trade depends on which token is passed into the initial input token configuration argument.

We have already defined a public .init() method. This method can be used in conjunction with the constructor to initialize the bot and start the price watch interval.

Here is a preview of how the client will look (you will write this in full in the Create Client section):

    const bot = new ArbBot({
solanaEndpoint: process.env.SOLANA_ENDPOINT ?? defaultConfig.solanaEndpoint,
metisEndpoint: process.env.METIS_ENDPOINT ?? defaultConfig.jupiter,
secretKey: decodedSecretKey,
firstTradePrice: 900_000, // min USDC out (6 decimals) — $0.90 for 0.01 SOL at ~$90/SOL
targetGainPercentage: 1.5,
initialInputToken: SwapToken.SOL,
initialInputAmount: 10_000_000, // 0.01 SOL in lamports
});

await bot.init();

Let's define the refreshBalances and initiatePriceWatch methods that we call in the constructor's init method.

Refresh Balances

The refreshBalances method will be used to fetch the current SOL and USDC balances of the bot's wallet. Add the following to bot.ts:

bot.ts (continued)
    private async refreshBalances(): Promise<void> {
try {
const results = await Promise.allSettled([
this.rpc.getBalance(this.wallet.address).send(),
this.rpc.getTokenAccountBalance(this.usdcTokenAccount).send()
]);

const solBalanceResult = results[0];
const usdcBalanceResult = results[1];

if (solBalanceResult.status === 'fulfilled') {
this.solBalance = solBalanceResult.value.value;
} else {
console.error('Error fetching SOL balance:', solBalanceResult.reason);
}

if (usdcBalanceResult.status === 'fulfilled') {
this.usdcBalance = usdcBalanceResult.value.value.uiAmount ?? 0;
} else {
this.usdcBalance = 0;
}

if (this.solBalance < LAMPORTS_PER_SOL / 100n) {
this.terminateSession("Low SOL balance.");
}
} catch (error) {
console.error('Unexpected error during balance refresh:', error);
}
}

Here's what we're doing:

  • Using Promise.allSettled to fetch the SOL and USDC balances in parallel, using the getBalance and getTokenAccountBalance methods on the Kit RPC client. Note that .send() is required to execute the RPC call. Kit uses a builder pattern that lets you configure the request before sending.
  • We then update the bot's SOL and USDC balances based on the results if they are successful, otherwise we log the error.
  • We also check if the SOL balance is less than 0.01 SOL and terminate the bot if it is.

Initiate Price Watch

The initiatePriceWatch method will be used to start the price watch interval. Add the following to bot.ts:

bot.ts (continued)
    private initiatePriceWatch(): void {
this.priceWatchIntervalId = setInterval(async () => {
const currentTime = Date.now();
if (currentTime - this.lastCheck >= this.checkInterval) {
this.lastCheck = currentTime;
try {
if (this.waitingForConfirmation) {
console.log('Waiting for previous transaction to confirm...');
return;
}
const quote = await this.getQuote(this.nextTrade);
this.evaluateQuoteAndSwap(quote);
} catch (error) {
console.error('Error getting quote:', error);
}
}
}, this.checkInterval);
}

This is just a simple interval that will call the getQuote method and then the evaluateQuoteAndSwap method if:

  • The time since the last check is greater than the check interval and
  • The bot is not waiting for confirmation before proceeding (we will include toggles for this in the executeSwap and postTransactionProcessing methods to ensure the bot does not attempt to execute a trade while waiting for confirmation).

Let's define the getQuote, evaluateQuoteAndSwap, and executeSwap methods next.

Get Quote

To get a quote, we will rely on Jupiter's quoteGet method. Add the following to bot.ts:

bot.ts (continued)
    private async getQuote(quoteRequest: QuoteGetRequest): Promise<QuoteResponse> {
try {
const quote: QuoteResponse | null = await this.jupiterApi.quoteGet(quoteRequest);
if (!quote) {
throw new Error('No quote found');
}
return quote;
} catch (error) {
if (error instanceof ResponseError) {
console.log(await error.response.json());
}
else {
console.error(error);
}
throw new Error('Unable to find quote');
}
}

This should look familiar to our example in the overview sections. We are simply passing the quote request to the quoteGet method and returning the quote if it exists, otherwise we log the error and throw a new error. If you refer back to initiatePriceWatch, you can see that we will pass this.nextTrade to this method. Our NextTrade interface extends the QuoteGetRequest interface, so we can pass it directly to the quoteGet method 🙌.

Evaluate Quote and Swap

We need a way to ensure a quote meets our conditions before executing a trade. We will define the evaluateQuoteAndSwap method to handle this. Add the following to bot.ts:

bot.ts (continued)
    private async evaluateQuoteAndSwap(quote: QuoteResponse): Promise<void> {
let difference = (parseInt(quote.outAmount) - this.nextTrade.nextTradeThreshold) / this.nextTrade.nextTradeThreshold;
console.log(`📈 Current price: ${quote.outAmount} is ${difference > 0 ? 'higher' : 'lower'
} than the next trade threshold: ${this.nextTrade.nextTradeThreshold} by ${Math.abs(difference * 100).toFixed(2)}%.`);
if (parseInt(quote.outAmount) > this.nextTrade.nextTradeThreshold) {
try {
this.waitingForConfirmation = true;
await this.executeSwap(quote);
} catch (error) {
console.error('Error executing swap:', error);
}
}
}

Our evaluateQuoteAndSwap method will accept the response from the quoteGet method and then calculate the difference between the quote's output amount and the next trade threshold. If the difference is positive, we will execute the swap. We will also set the waitingForConfirmation flag to true to prevent the bot from attempting to execute another trade while waiting for confirmation. For debugging/demonstration, we will also log the current price and the difference between the current price and the next trade threshold.

Confirm Transaction

Rather than writing a manual polling loop, we use Kit's sendAndConfirmTransactionFactory. We initialize it in init() alongside the wallet:

bot.ts (continued)
this.sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc: this.rpc,
rpcSubscriptions: this.rpcSubscriptions,
});

Under the hood it uses the WebSocket subscriptions client to watch for the transaction signature and throws if the transaction fails or times out. We call it in executeSwap after signing.

Execute Swap

Finally, if our bot detects that market conditions are appropriate to satisfy our trade requirements, we should execute the trade. We are going to pack a lot into this method:

  1. Fetch the swap instructions from Jupiter's API
  2. Refactor our received instruction data to transaction instructions
  3. Fetch the address lookup table accounts
  4. Create and send a Solana Transaction
  5. On success, log the swap and update the next trade conditions

Let's add our code, and then we will break it down:

bot.ts (continued)
    private async executeSwap(route: QuoteResponse): Promise<void> {
try {
const {
computeBudgetInstructions,
setupInstructions,
swapInstruction,
cleanupInstruction,
addressLookupTableAddresses,
} = await this.jupiterApi.swapInstructionsPost({
swapRequest: {
quoteResponse: route,
userPublicKey: this.wallet.address,
prioritizationFeeLamports: {
priorityLevelWithMaxLamports: {
priorityLevel: 'high',
maxLamports: 1_000_000,
}
}
},
});

const instructions: Instruction[] = [
...computeBudgetInstructions.map(this.jupiterInstructionToKitInstruction.bind(this)),
...setupInstructions.map(this.jupiterInstructionToKitInstruction.bind(this)),
this.jupiterInstructionToKitInstruction(swapInstruction),
this.jupiterInstructionToKitInstruction(cleanupInstruction),
].filter((ix): ix is Instruction => ix !== null);

const alts = await this.getAddressLookupTableAccounts(addressLookupTableAddresses);

const { value: latestBlockhash } = await this.rpc.getLatestBlockhash().send();

const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayerSigner(this.wallet, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
(m) => appendTransactionMessageInstructions(instructions, m),
(m) => compressTransactionMessageUsingAddressLookupTables(m, alts),
);

const signedTx = await signTransactionMessageWithSigners(message);
const signature = getSignatureFromTransaction(signedTx);
await this.sendAndConfirmTransaction(signedTx, { commitment: 'confirmed', skipPreflight: true });
await this.postTransactionProcessing(route, signature);
} catch (error) {
if (error instanceof ResponseError) {
console.log(await error.response.json());
}
else {
console.error(error);
}
throw new Error('Unable to execute swap');
} finally {
this.waitingForConfirmation = false;
}
}

  • First, we are fetching the swap instructions from Jupiter's API by calling this.jupiterApi.swapInstructionsPost. We are passing in the quote received from the getQuote method, our wallet's address (necessary to build a user-specific instruction set), and a prioritization fee (we are using priorityLevelWithMaxLamports with a high priority level and a cap of 1,000,000 lamports). You can explore additional optional arguments by checking the Jupiter API Docs.
  • We then convert the Jupiter instruction objects to Kit IInstruction types using jupiterInstructionToKitInstruction. This maps each account's isSigner/isWritable flags to Kit's AccountRole enum and decodes the base64 instruction data to a Uint8Array.
  • We fetch the address lookup table accounts using getAddressLookupTableAccounts. Complex swap routes reference many pool and vault accounts; the ALT data lets us compress those addresses in the final transaction and stay within Solana's 1232-byte transaction size limit.
  • We build the transaction message using Kit's pipe pattern: create a V0 message, set the fee payer signer (which also embeds the signing key for the next step), attach the blockhash lifetime, append instructions, then compress with address lookup tables.
  • We sign with signTransactionMessageWithSigners (which reads the embedded signer from the fee payer), extract the signature for logging, then send and confirm in one call.
  • After confirming the transaction was successful, we call the postTransactionProcessing method to trigger necessary steps after a successful swap (updateNextTrade, refreshBalances, and logSwap). We already have refreshBalances and logSwap defined, so we will define updateNextTrade in the next section.

Update Next Trade

Finally, after a trade executes, we need to change our arguments for the next swap (redefining our NextTrade interface). We will define the updateNextTrade method to handle this. Add the following to bot.ts:

bot.ts (continued)
    private async updateNextTrade(lastTrade: QuoteResponse): Promise<void> {
const priceChange = this.targetGainPercentage / 100;
this.nextTrade = {
inputMint: this.nextTrade.outputMint,
outputMint: this.nextTrade.inputMint,
amount: parseInt(lastTrade.outAmount),
slippageBps: 300,
nextTradeThreshold: parseInt(lastTrade.inAmount) * (1 + priceChange),
};
}

For simplicity in this example, we will just swap the input and output mints (meaning, if we were buying SOL with USDC before, the next swap should sell USDC for SOL). We will also set the next trade thresholds. Our amount is the number of tokens we will put into our next trade. We are defining that as the amount of tokens we got out of the previous trade. Our nextTradeThreshold is the price at which we will execute our next trade. We define that as the number of tokens we put into our previous trade plus our target gain percentage. For example, if we used 10 USDC to buy 0.1 SOL in our previous trade and our target gain percentage is 15%; our next trade input (amount) will be 0.1 SOL, and the next trade threshold will be 11.5 USDC (meaning we expect our next trigger to yield 11.5 USDC).

Great work! You have now defined the core logic of our trading bot. All we need to do is create a client and run the bot. Let's do that now.

Create Client

Open index.ts and add the following code:

index.ts
import { ArbBot, SwapToken } from './bot';

const defaultConfig = {
solanaEndpoint: 'https://api.mainnet-beta.solana.com',
jupiter: "https://quote-api.jup.ag/v6",
};

async function main() {
if (!process.env.SECRET_KEY) {
throw new Error("SECRET_KEY environment variable not set");
}
let decodedSecretKey = Uint8Array.from(JSON.parse(process.env.SECRET_KEY));

const bot = new ArbBot({
solanaEndpoint: process.env.SOLANA_ENDPOINT ?? defaultConfig.solanaEndpoint,
metisEndpoint: process.env.METIS_ENDPOINT ?? defaultConfig.jupiter,
secretKey: decodedSecretKey,
firstTradePrice: 900_000, // min USDC out (6 decimals) — $0.90 for 0.01 SOL at ~$90/SOL
targetGainPercentage: 1.5,
initialInputToken: SwapToken.SOL,
initialInputAmount: 10_000_000, // 0.01 SOL in lamports
});

await bot.init();

}

main().catch(console.error);

This simple client will create an instance of our ArbBot and call the init method. Environment variables are loaded from the .env file at startup via Node's --env-file flag. We have included a default configuration object that will be used if the environment variables are not set. Let's explain our other input parameters to make sure we understand what's happening:

  • firstTradePrice is the minimum output amount we expect to receive in our first trade, expressed in the output token's smallest unit. In our example, the bot sells SOL for USDC, so 900_000 means we expect at least 0.90 USDC (6 decimals) out.
  • targetGainPercentage is the percentage gain we want to achieve in our trades. In our example, we are setting this to 1.5%. This means subsequent trades will be triggered when the price moves 1.5% above the previous trade's input amount.
  • initialInputToken is the token we will use to initiate our first trade. In our example, we are setting this to SOL.
  • initialInputAmount is the amount of tokens we will use to initiate our first trade, in the token's smallest unit. In our example, 10_000_000 lamports = 0.01 SOL.

In short, we are setting up our bot to sell 0.01 SOL and expect at least 0.90 USDC out. Subsequent trades will be triggered when the output amount is 1.5% above the previous trade's input amount.

Run the Bot


Trading on Mainnet

Currently, Jupiter trading API is only available on Mainnet, meaning any trades executed will be real and irreversible. Please ensure you have a good understanding of the bot's logic and the potential risks before running it on Mainnet.

In your terminal, run the following command to start the bot:

npx tsx --env-file=.env index.ts

And that's it! You should see our 🤖 log initiating the bot and regular logs for price updates and confirmation of successful trades!

🤖 Initiating arb bot for wallet: JUPz...Q1ie.
🏦 Current balances:
SOL: 0.01271548,
USDC: 10.087
📈 Current price: 97624457 is lower than the next trade threshold: 100000000 by 2.38%.
✅ Logged swap: 10000000 So11111111111111111111111111111111111111112 -> 910723 EPjFW...Dt1v,
TX: BoC7...i2nr

You can find our complete code on our GitHub.

Frequently Asked Questions

What is Jupiter API and how does it integrate with Quicknode?

Metis is Jupiter's onchain routing engine, now an independent project at metis.builders. It exposes an HTTP API (/quote, /swap-instructions) for fetching swap quotes and building Solana transactions. Quicknode's Metis add-on hosts the Metis binary on Quicknode's infrastructure, giving you a private, managed endpoint that works alongside your Solana RPC endpoint without any setup on your part.

How does the trading strategy work in the example bot?

The bot starts with an initial token (SOL or USDC), monitors prices at set intervals using Jupiter quotes, and swaps when the price meets a target gain threshold (e.g., 1.5% profit). After a successful trade, it updates conditions for the reverse trade and logs all swap details to a JSON file.

What role does Quicknode's Metis add-on play?

Quicknode's Metis add-on hosts the Metis binary (Jupiter's onchain routing engine) on Quicknode's infrastructure. It provides the basePath for the @jup-ag/api client and exposes the same /quote and /swap-instructions endpoints the bot uses. The add-on runs on its own Solana validator node with rolling restarts and low latency, and doesn't use your account credits.

How does the bot execute a swap transaction?

The bot fetches swap instructions from Jupiter, converts them to Solana transaction instructions, adds address lookup tables, signs the transaction with your wallet, and sends it. Upon success, it confirms the transaction, logs the swap details, refreshes balances, and updates next trade conditions.

Is Jupiter API free to use?

Jupiter API can be accessed through Quicknode's Metis add-on (which doesn't use your account credits) or through a public endpoint at jupiterapi.com. The guide uses Quicknode's infrastructure for enhanced performance and reliability.

Wrap Up

You have now experimented with the Jupiter API and Quicknode's Metis add-on. You have also built a simple trading bot that uses Jupiter's API to monitor the market for specific conditions and execute trades when those conditions are met. You can now experiment with different trade conditions and strategies to see how the bot performs. Looking for inspiration? Here are a few ideas:

Resources

If you have a question or idea you want to share, drop us a line on Discord or Twitter!

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide