Skip to main content

What You’ll Build

By the end of this tutorial, you’ll know how to programmatically:
  • Zap-In — Find a pool and add liquidity with a single token (SOL)
  • Zap-Out — Withdraw your liquidity back to your wallet
No smart contract knowledge required — LP Agent handles everything through simple REST APIs.
LP Agent provides built-in transaction landing via Jito bundles for both zap-in and zap-out. This gives you a significantly better landing rate than submitting transactions directly via RPC — and it’s free to use.

How It Works

The Zap-In Flow

Adding liquidity to a Meteora pool involves 4 steps:
1. DISCOVER       Find a pool that matches your criteria
   GET /pools/discover

2. GET POOL INFO   Get the current active bin (price) to set your range
   GET /pools/{poolId}/info

3. GENERATE TXS   API creates unsigned transactions for you
   POST /pools/{poolId}/add-tx

4. SIGN & LAND     Sign with your wallet, submit via Jito
   POST /pools/landing-add-tx
What is “zap-in”? You provide just SOL, and the API automatically swaps it into the correct ratio of both tokens before adding liquidity. No need to manually buy tokens first.

The Zap-Out Flow

Withdrawing liquidity follows a similar pattern:
1. GET POSITIONS   Find your open positions
   GET /lp-positions/opening

2. GET QUOTES      (Optional) Preview what you'll receive
   POST /position/decrease-quotes

3. GENERATE TXS   API creates unsigned withdrawal transactions
   POST /position/decrease-tx

4. SIGN & LAND     Sign with your wallet, submit via Jito
   POST /position/landing-decrease-tx

Understanding Key Concepts

Before diving into code, here are the key concepts:
ConceptWhat It Means
BinA price point in a Meteora DLMM pool. Liquidity is placed across a range of bins.
Active BinThe bin where the current price sits. Your position earns fees when the price is within your bin range.
StrategyHow liquidity is distributed across bins — Spot (uniform), Curve (concentrated in middle), or BidAsk (concentrated at edges).
BPSBasis points. 10000 = 100%, 5000 = 50%, 500 = 5%. Used for slippage and withdrawal amounts.
LandingSubmitting signed transactions on-chain. LP Agent uses Jito bundles for better success rates.

Prerequisites

  • An LP Agent API key (get one from the API Dashboard)
  • A Solana wallet keypair
  • Node.js >= 18
npm install @solana/web3.js bs58

Step-by-Step: Zap-In

Step 1: Setup

First, set up your API client and wallet:
import { Keypair, Transaction, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";

const API_BASE = "https://api.lpagent.io/open-api/v1";
const API_KEY = "your-api-key-here";

const wallet = Keypair.fromSecretKey(bs58.decode("your-base58-private-key"));
const OWNER = wallet.publicKey.toBase58();

// Helper for API calls
async function apiCall(method: string, path: string, body?: object) {
  const res = await fetch(`${API_BASE}${path}`, {
    method,
    headers: {
      "Content-Type": "application/json",
      "x-api-key": API_KEY,
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  if (!res.ok) {
    const error = await res.json();
    throw new Error(`API error: ${JSON.stringify(error)}`);
  }

  return res.json();
}

// Sign a base64-encoded transaction (handles both legacy and versioned)
function signTransaction(base64Tx: string): string {
  const buffer = Buffer.from(base64Tx, "base64");

  try {
    const tx = VersionedTransaction.deserialize(buffer);
    tx.sign([wallet]);
    return Buffer.from(tx.serialize()).toString("base64");
  } catch {
    const tx = Transaction.from(buffer);
    tx.partialSign(wallet);
    return tx
      .serialize({ requireAllSignatures: false, verifySignatures: false })
      .toString("base64");
  }
}

Step 2: Discover a Pool

Browse available pools and filter by your criteria. The API returns pools sorted by your chosen metric.
const discoverRes = await apiCall("GET",
  "/pools/discover?" + new URLSearchParams({
    chain: "SOL",
    sortBy: "vol_24h",        // sort by 24h volume
    sortOrder: "desc",
    pageSize: "5",
    min_market_cap: "5",       // min $5M market cap
    min_liquidity: "50",       // min $50K TVL
  }).toString()
);

const pool = discoverRes.data[0];
console.log(`Pool: ${pool.token0_symbol}/${pool.token1_symbol} (${pool.pool})`);
console.log(`Protocol: ${pool.protocol}`); // "meteora" or "meteora_damm_v2"
What you get back: A list of pools with their addresses, token pairs, TVL, volume, and protocol type. The API supports both Meteora DLMM and DAMM V2 pools — the same code works for both.

Step 3: Get Pool Info & Set Your Range

Fetch the pool’s current state to find the active bin (where the current price is). You’ll place liquidity around this bin.
const poolInfoRes = await apiCall("GET", `/pools/${pool.pool}/info`);
const activeBin = poolInfoRes.data.liquidityViz?.activeBin;

// Place liquidity 34 bins on each side of the current price
const RANGE = 34;
const fromBinId = activeBin.binId - RANGE;
const toBinId = activeBin.binId + RANGE;

console.log(`Active bin: ${activeBin.binId}`);
console.log(`Your range: bin ${fromBinId} to ${toBinId} (${RANGE * 2 + 1} bins)`);
How to choose your range: Wider = less risk of going out of range, but lower fee APR. Tighter = higher fees but more frequent rebalancing needed. Start with 30-70 bins on each side.

Step 4: Generate Zap-In Transactions

Tell the API how much SOL you want to deposit. It generates unsigned transactions that swap your SOL into the right token ratio and add liquidity.
const addTxRes = await apiCall("POST", `/pools/${pool.pool}/add-tx`, {
  stratergy: "Spot",           // distribution strategy
  inputSOL: 0.1,               // amount of SOL to deposit
  percentX: 0.5,               // 50/50 split between tokens
  fromBinId,
  toBinId,
  owner: OWNER,
  slippage_bps: 500,           // 5% slippage tolerance
  mode: "zap-in",              // auto-swap SOL to both tokens
});

console.log(`Position key: ${addTxRes.data.meta.positionPubKey}`);
console.log(`Swap txs: ${addTxRes.data.swapTxsWithJito.length}`);
console.log(`Add txs: ${addTxRes.data.addLiquidityTxsWithJito.length}`);
Already holding both tokens? Use mode: "normal" with amountX and amountY instead of inputSOL to skip the swap step.
What you get back: Base64-encoded unsigned transactions, split into swap transactions (to convert SOL) and add-liquidity transactions. Plus metadata about your new position.

Step 5: Sign & Land

Sign the transactions with your wallet and submit them via LP Agent’s Jito landing endpoint.
const { lastValidBlockHeight, swapTxsWithJito, addLiquidityTxsWithJito, meta } = addTxRes.data;

// Sign all transactions with your wallet
const signedSwapTxs = swapTxsWithJito.map(signTransaction);
const signedAddTxs = addLiquidityTxsWithJito.map(signTransaction);

// Submit via LP Agent's Jito integration (better landing rate — free!)
const landRes = await apiCall("POST", "/pools/landing-add-tx", {
  lastValidBlockHeight,
  swapTxsWithJito: signedSwapTxs,
  addLiquidityTxsWithJito: signedAddTxs,
  meta,
});

console.log(`Zap-In Success! Tx: https://solscan.io/tx/${landRes.data.signature}`);
That’s it! Your liquidity is now live and earning fees.

Step-by-Step: Zap-Out

Step 1: Find Your Open Positions

Query your wallet to see all open LP positions.
const positionsRes = await apiCall("GET",
  `/lp-positions/opening?owner=${OWNER}`
);

console.log(`Found ${positionsRes.count} open positions`);

for (const pos of positionsRes.data) {
  console.log(`- ${pos.pairName} | Value: $${pos.currentValue} | PnL: ${pos.pnl.percent.toFixed(2)}% | In Range: ${pos.inRange}`);
}
What you get back: Each position includes its encrypted ID (needed for withdrawal), current value, PnL, whether it’s in range, and token details.

Step 2: Get Quotes (Optional)

Preview what you’ll receive before withdrawing. The API shows quotes for different output options.
const positionId = positionsRes.data[0].id; // encrypted position ID

const quotesRes = await apiCall("POST", "/position/decrease-quotes", {
  id: positionId,
  bps: 10000,    // 10000 = 100% withdrawal, 5000 = 50%, etc.
});

const quotes = quotesRes.data;
console.log(`Token prices: $${quotes.price.token0} / $${quotes.price.token1}`);

Step 3: Generate Zap-Out Transactions

Choose how much to withdraw and what token(s) to receive.
const decreaseTxRes = await apiCall("POST", "/position/decrease-tx", {
  position_id: positionId,
  bps: 10000,                  // 100% withdrawal
  owner: OWNER,
  slippage_bps: 500,
  output: "allBaseToken",      // withdraw to SOL (see table below)
  provider: "JUPITER_ULTRA",
});

console.log(`Generated ${decreaseTxRes.data.closeTxsWithJito.length} close txs`);
Output options:
OutputDescription
allBaseTokenSwap everything to SOL (recommended for easy re-entry)
bothReceive both tokens as-is (no swap)
allToken0Swap everything to token X
allToken1Swap everything to token Y

Step 4: Sign & Land

const { lastValidBlockHeight: blockHeight, closeTxsWithJito, swapTxsWithJito } = decreaseTxRes.data;

// Sign all transactions
const signedCloseTxs = closeTxsWithJito.map(signTransaction);
const signedSwapTxs = swapTxsWithJito.map(signTransaction);

// Submit via LP Agent's Jito integration
const landRes = await apiCall("POST", "/position/landing-decrease-tx", {
  lastValidBlockHeight: blockHeight,
  closeTxs: [],
  swapTxs: [],
  closeTxsWithJito: signedCloseTxs,
  swapTxsWithJito: signedSwapTxs,
});

console.log(`Zap-Out Success! Tx: https://solscan.io/tx/${landRes.data.signature}`);
Always use the landing-decrease-tx endpoint instead of submitting transactions directly via RPC. LP Agent’s Jito integration provides a much better landing rate and it’s completely free.

Reference

Strategy Types

StrategyDescriptionBest For
SpotUniform distribution across all binsStable pairs (SOL/USDC)
CurveBell curve concentrated around active binRange-bound markets
BidAskHeavier allocation at the edgesVolatile pairs with mean reversion

Common Errors

ErrorCauseFix
Token X price unavailablePrice feed missingTry again later or use a different pool
Insufficient balanceNot enough tokensReduce inputSOL or amountX/amountY
Missing amount to ZapIninputSOL not provided in zap-in modeAdd inputSOL to the request body
Transaction expiredlastValidBlockHeight has passedRe-generate and re-sign (don’t wait more than ~60s)

Tips

  • Transaction landing: Always use LP Agent’s landing endpoints instead of direct RPC. Better success rates via Jito — for free.
  • DAMM V2 pools: Both zap-in and zap-out auto-detect pool types. The same code works for DLMM and DAMM V2.
  • Partial zap-out: Use bps less than 10000 to withdraw only a portion (e.g., 5000 = 50%).

Complete Script

Here’s everything above combined into a single copy-paste-ready script:
import { Keypair, Transaction, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";

const API_BASE = "https://api.lpagent.io/open-api/v1";
const API_KEY = "your-api-key-here";
const wallet = Keypair.fromSecretKey(bs58.decode("your-base58-private-key"));
const OWNER = wallet.publicKey.toBase58();

async function apiCall(method: string, path: string, body?: object) {
  const res = await fetch(`${API_BASE}${path}`, {
    method,
    headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) throw new Error(`API error: ${await res.text()}`);
  return res.json();
}

function signTx(base64Tx: string): string {
  const buffer = Buffer.from(base64Tx, "base64");
  try {
    const tx = VersionedTransaction.deserialize(buffer);
    tx.sign([wallet]);
    return Buffer.from(tx.serialize()).toString("base64");
  } catch {
    const tx = Transaction.from(buffer);
    tx.partialSign(wallet);
    return tx.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
  }
}

// ===================== ZAP-IN =====================
async function zapIn(poolAddress: string, amountSOL: number) {
  const info = await apiCall("GET", `/pools/${poolAddress}/info`);
  const activeBin = info.data.liquidityViz?.activeBin;
  const fromBinId = activeBin.binId - 34;
  const toBinId = activeBin.binId + 34;

  const addTx = await apiCall("POST", `/pools/${poolAddress}/add-tx`, {
    stratergy: "Spot",
    inputSOL: amountSOL,
    percentX: 0.5,
    fromBinId,
    toBinId,
    owner: OWNER,
    slippage_bps: 500,
    mode: "zap-in",
  });

  const signedSwapTxs = addTx.data.swapTxsWithJito.map(signTx);
  const signedAddTxs = addTx.data.addLiquidityTxsWithJito.map(signTx);

  const result = await apiCall("POST", "/pools/landing-add-tx", {
    lastValidBlockHeight: addTx.data.lastValidBlockHeight,
    swapTxsWithJito: signedSwapTxs,
    addLiquidityTxsWithJito: signedAddTxs,
    meta: addTx.data.meta,
  });

  console.log(`Zap-In done: https://solscan.io/tx/${result.data.signature}`);
  return addTx.data.meta.positionPubKey;
}

// ===================== ZAP-OUT =====================
async function zapOut(positionId: string, bps: number = 10000) {
  const decreaseTx = await apiCall("POST", "/position/decrease-tx", {
    position_id: positionId,
    bps,
    owner: OWNER,
    slippage_bps: 500,
    output: "allBaseToken",
  });

  const signedCloseTxs = decreaseTx.data.closeTxsWithJito.map(signTx);
  const signedSwapTxs = decreaseTx.data.swapTxsWithJito.map(signTx);

  const result = await apiCall("POST", "/position/landing-decrease-tx", {
    lastValidBlockHeight: decreaseTx.data.lastValidBlockHeight,
    closeTxs: [],
    swapTxs: [],
    closeTxsWithJito: signedCloseTxs,
    swapTxsWithJito: signedSwapTxs,
  });

  console.log(`Zap-Out done: https://solscan.io/tx/${result.data.signature}`);
}

// ===================== MAIN =====================
async function main() {
  // Discover a pool
  const discover = await apiCall("GET", "/pools/discover?" + new URLSearchParams({
    chain: "SOL", sortBy: "vol_24h", sortOrder: "desc", pageSize: "1",
  }));
  const pool = discover.data[0];
  console.log(`Pool: ${pool.token0_symbol}/${pool.token1_symbol}`);

  // Zap-In: add 0.1 SOL of liquidity
  const positionKey = await zapIn(pool.pool, 0.1);
  console.log(`Position created: ${positionKey}`);

  // Check position status
  const positions = await apiCall("GET", `/lp-positions/opening?owner=${OWNER}`);
  const position = positions.data.find((p: any) => p.pool === pool.pool);
  if (position) {
    console.log(`Position value: $${position.currentValue}, PnL: ${position.pnl.percent}%`);

    // Zap-Out: withdraw 100% to SOL
    await zapOut(position.id, 10000);
  }
}

main().catch(console.error);

Next Steps