Skip to main content
The /build endpoint returns raw swap instructions instead of an assembled transaction. You build the transaction yourself, which gives you full control to add custom instructions, CPI, or modify any part of the transaction.

Quick start

Prerequisites

import {
  AccountRole,
  Address,
  address,
  AddressesByLookupTableAddress,
  appendTransactionMessageInstructions,
  Base64EncodedBytes,
  Blockhash,
  compileTransaction,
  compressTransactionMessageUsingAddressLookupTables,
  createKeyPairSignerFromBytes,
  createSolanaRpc,
  createTransactionMessage,
  getBase58Decoder,
  getBase58Encoder,
  getBase64Codec,
  getBase64EncodedWireTransaction,
  AccountMeta,
  Instruction,
  pipe,
  setTransactionMessageFeePayer,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransaction,
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system"; // For adding custom instructions

// ── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = "https://api.jup.ag/swap/v2";
const COMPUTE_BUDGET_PROGRAM: Address = address(
  "ComputeBudget111111111111111111111111111111",
);
const COMPUTE_UNIT_LIMIT_MAX = 1_400_000;
const CU_BUFFER_NUMERATOR = 120n;
const CU_BUFFER_DENOMINATOR = 100n; // 120% of simulated CUs

// ── Instruction types ───────────────────────────────────────────────────────

type Account = {
  pubkey: Address;
  isSigner: boolean;
  isWritable: boolean;
};

type ApiInstruction = {
  programId: Address;
  accounts: Account[];
  data: Base64EncodedBytes;
};

function toAccountMeta(acc: Account): AccountMeta {
  const role =
    acc.isSigner && acc.isWritable
      ? AccountRole.WRITABLE_SIGNER
      : acc.isSigner
        ? AccountRole.READONLY_SIGNER
        : acc.isWritable
          ? AccountRole.WRITABLE
          : AccountRole.READONLY;
  return { address: acc.pubkey, role };
}

function createInstruction(
  ix: ApiInstruction,
): Instruction<string, readonly AccountMeta[]> {
  return {
    programAddress: ix.programId,
    accounts: ix.accounts.map(toAccountMeta),
    data: Uint8Array.from(getBase64Codec().encode(ix.data)),
  };
}

// ── Build response type ─────────────────────────────────────────────────────

type BuildResponse = {
  inputMint: string;
  outputMint: string;
  inAmount: string;
  outAmount: string;
  otherAmountThreshold: string;
  swapMode: string;
  slippageBps: number;
  routePlan: {
    percent: number;
    bps: number;
    swapInfo: {
      ammKey: string;
      label: string;
      inputMint: string;
      outputMint: string;
      inAmount: string;
      outAmount: string;
    };
  }[];
  computeBudgetInstructions: ApiInstruction[];
  setupInstructions: ApiInstruction[];
  swapInstruction: ApiInstruction;
  cleanupInstruction: ApiInstruction | null;
  otherInstructions: ApiInstruction[];
  addressesByLookupTableAddress: Record<string, string[]> | null;
  blockhashWithMetadata: {
    blockhash: number[];
    lastValidBlockHeight: number;
  };
};

// ── Helpers ──────────────────────────────────────────────────────────────────

function makeSetComputeUnitLimitIx(units: number): ApiInstruction {
  const data = Buffer.alloc(5);
  data.writeUInt8(0x02, 0);
  data.writeUInt32LE(units, 1);
  return {
    programId: COMPUTE_BUDGET_PROGRAM,
    accounts: [],
    data: data.toString("base64") as ApiInstruction["data"],
  };
}

function transformBlockhash(meta: BuildResponse["blockhashWithMetadata"]): {
  blockhash: Blockhash;
  lastValidBlockHeight: bigint;
} {
  return {
    blockhash: getBase58Decoder().decode(
      Uint8Array.from(meta.blockhash),
    ) as Blockhash,
    lastValidBlockHeight: BigInt(meta.lastValidBlockHeight),
  };
}

function transformALTs(
  raw: Record<string, string[]> | null,
): AddressesByLookupTableAddress {
  if (!raw) return {};
  return Object.fromEntries(
    Object.entries(raw).map(([key, addrs]) => [
      address(key),
      addrs.map((a) => address(a)),
    ]),
  );
}

function buildTransaction(
  ixs: Instruction<string, readonly AccountMeta[]>[],
  blockhash: { blockhash: Blockhash; lastValidBlockHeight: bigint },
  alts: AddressesByLookupTableAddress,
  feePayer: Address,
) {
  return pipe(
    createTransactionMessage({ version: 0 }),
    (msg) => appendTransactionMessageInstructions(ixs, msg),
    (msg) => compressTransactionMessageUsingAddressLookupTables(msg, alts),
    (msg) => setTransactionMessageFeePayer(feePayer, msg),
    (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash, msg),
    (msg) => compileTransaction(msg),
  );
}

Code example

const API_KEY = process.env.JUPITER_API_KEY;
const RPC_URL = process.env.RPC_URL;
if (!API_KEY) throw new Error("Missing JUPITER_API_KEY");
if (!RPC_URL) throw new Error("Missing RPC_URL");

const signer = await createKeyPairSignerFromBytes(
  getBase58Encoder().encode(process.env.BS58_PRIVATE_KEY!),
);
const rpc = createSolanaRpc(RPC_URL);

// Step 1: Get swap instructions from /build
const buildRes = await fetch(
  `${BASE_URL}/build?` +
    new URLSearchParams({
      inputMint: "So11111111111111111111111111111111111111112",
      outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      amount: "100000000",
      taker: signer.address,
      slippageBps: "100",
    }),
  { headers: { "x-api-key": API_KEY } },
);
if (!buildRes.ok) {
  console.error(`/build failed: ${buildRes.status}`, await buildRes.text());
  process.exit(1);
}
const build: BuildResponse = await buildRes.json();

// Step 2: Collect instructions (no compute unit limit yet, we simulate first)
// Example: add a SOL transfer after the swap
const RECIPIENT = address("YOUR_RECIPIENT_ADDRESS");
const transferIx = getTransferSolInstruction({
  source: signer,
  destination: RECIPIENT,
  amount: 1_000_000n, // 0.001 SOL
});

const instructions = [
  ...build.setupInstructions.map(createInstruction),
  createInstruction(build.swapInstruction),
  transferIx, // Your custom instruction, added after the swap
  ...(build.cleanupInstruction
    ? [createInstruction(build.cleanupInstruction)]
    : []),
  ...build.otherInstructions.map(createInstruction),
];

// Step 3: Prepare blockhash and address lookup tables
const blockhash = transformBlockhash(build.blockhashWithMetadata);
const alts = transformALTs(build.addressesByLookupTableAddress);

// Step 4: Simulate with max CU limit to estimate actual usage
const simulationTx = buildTransaction(
  [
    createInstruction(makeSetComputeUnitLimitIx(COMPUTE_UNIT_LIMIT_MAX)),
    ...instructions,
  ],
  blockhash,
  alts,
  signer.address,
);

const simulationResult = await rpc
  .simulateTransaction(getBase64EncodedWireTransaction(simulationTx), {
    encoding: "base64",
    commitment: "confirmed",
    replaceRecentBlockhash: true,
  })
  .send();

if (simulationResult.value.err) {
  console.error("Simulation failed:", simulationResult.value.err);
}

// Set 1.2x buffer on simulated CU, capped at max
const estimatedCUL = simulationResult.value.unitsConsumed
  ? Math.min(
      Number(
        (simulationResult.value.unitsConsumed * CU_BUFFER_NUMERATOR) /
          CU_BUFFER_DENOMINATOR,
      ),
      COMPUTE_UNIT_LIMIT_MAX,
    )
  : COMPUTE_UNIT_LIMIT_MAX;
console.log(
  `Simulated CUs: ${simulationResult.value.unitsConsumed}, using limit: ${estimatedCUL}`,
);

// Step 5: Build final transaction with estimated CU limit + CU price from response
const compiledTx = buildTransaction(
  [
    createInstruction(makeSetComputeUnitLimitIx(estimatedCUL)),
    ...build.computeBudgetInstructions.map(createInstruction),
    ...instructions,
  ],
  blockhash,
  alts,
  signer.address,
);

// Step 6: Sign
const signedTransaction = await signTransaction([signer.keyPair], compiledTx);

// Step 7: Send via your own RPC
const wireTransaction = getBase64EncodedWireTransaction(signedTransaction);
const signature = await rpc
  .sendTransaction(wireTransaction, {
    encoding: "base64",
    skipPreflight: true,
  })
  .send();

console.log(`Transaction sent: https://solscan.io/tx/${signature}`);

How it works

1. Call /build

GET /build returns a quote and all the instructions you need to build a swap transaction. Required parameters:
ParameterDescription
inputMintMint address of the token you are selling
outputMintMint address of the token you are buying
amountAmount in the smallest unit of the input token
takerYour wallet address
Key response fields:
FieldDescription
computeBudgetInstructionsCompute unit price instruction (does not include compute unit limit)
setupInstructionsPre-swap setup (e.g. ATA creation)
swapInstructionThe main swap instruction
cleanupInstructionPost-swap cleanup (may be null)
otherInstructionsAdditional instructions
addressesByLookupTableAddressAddress lookup tables for v0 transactions
blockhashWithMetadataRecent blockhash and expiry height
Each instruction follows this structure:
{
  programId: string,     // Program address
  accounts: [
    {
      pubkey: string,    // Account address
      isWritable: boolean,
      isSigner: boolean
    }
  ],
  data: string           // Base64-encoded instruction data
}

2. Add your own instructions

Insert custom instructions alongside the swap instructions. Common examples:
  • SOL transfer (tip or payment)
  • Memo instruction
  • Create or close token accounts
  • Custom program CPI
See Other Instructions for a reference of common instructions.

3. Simulate compute unit limit

The /build response includes computeBudgetInstructions with the compute unit price but not the compute unit limit. You need to simulate the transaction to determine the correct limit. Why: integrators using /build typically add their own instructions, so the CU usage will differ from the base swap. The simulation gives you the actual CU consumed, and you set the limit to 1.2x that value (capped at 1,400,000). Both code examples above demonstrate this: simulate with max CU limit, then rebuild with 1.2x the simulated value plus the CU price from the response. See Solana fee structure for more on compute units and Estimate Compute Units for a standalone guide.

4. Build a v0 transaction

The response includes address lookup tables in addressesByLookupTableAddress. Use these to compile a v0 (versioned) transaction, which supports more accounts than legacy transactions.

5. Sign and send

Sign the transaction with your wallet and send via your own RPC.
/build transactions cannot use /execute. You are responsible for sending the transaction via your own RPC and handling confirmation. Jupiter does not charge swap fees on /build.

Key differences from /order

/order/build
RoutingAll routers (Metis + RFQ)Metis only
Swap feesJupiter platform feeNone
ExecutionManaged via /executeSelf-managed
Transaction controlNoneFull
Compute budgetIncluded in transactionIncluded as instructions (you can override)

Optional parameters

ParameterDefaultDescription
slippageBps50Slippage tolerance in basis points
mode(default)“fast” for reduced latency routing
maxAccounts64Maximum accounts for the swap route (1-64)
platformFeeBps0Integrator platform fee in bps (requires feeAccount)
feeAccount-Token account to collect platform fees
payerpublic keyAccount that pays transaction fees and rent
wrapAndUnwrapSoltrueWhether to wrap/unwrap SOL automatically
dexes(all)Restrict routing to specific DEXes
excludeDexes(none)Exclude specific DEXes from routing
destinationTokenAccount-SPL token account for output
nativeDestinationAccount-Native SOL account for output
blockhashSlotsToExpiry150Slots until blockhash expires (1-300)
For the full parameter reference, see the API reference. For how parameters affect routing, see Routing.