> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lpagent.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Auto-Rebalancing Bot

> Build a bot that monitors your LP positions and automatically rebalances when they go out of range

## What You'll Build

An auto-rebalancing bot that runs on a loop and:

1. **Monitors** your open LP positions every 60 seconds
2. **Detects** when a position goes out of range (stops earning fees)
3. **Zap-Out** — withdraws liquidity back to SOL
4. **Zap-In** — re-enters with a new position centered around the current price

No smart contract knowledge needed — the bot uses LP Agent's Open API for everything.

<Tip>
  All transactions are landed via LP Agent's built-in Jito integration, which provides a **significantly better landing rate** than submitting directly via RPC — and it's completely **free**.
</Tip>

***

## How Rebalancing Works

When the price moves outside your position's bin range, the position **stops earning fees**. A rebalance fixes this by closing the old position and opening a new one centered around the current price.

```
Price moves out of range:
  [-----old position-----]
                              ^ current price (no fees!)

After rebalance:
                     [-----new position-----]
                              ^ current price (earning fees again!)
```

### The Bot's Decision Loop

Every check interval, the bot follows this flow:

```
 ┌─────────────────────────────┐
 │  Fetch all open positions   │
 │  GET /lp-positions/opening  │
 └──────────┬──────────────────┘
            │
            ▼
 ┌─────────────────────────────┐
 │  For each position:         │
 │  Is it in range?            │
 └──────┬────────────┬─────────┘
        │ YES        │ NO
        ▼            ▼
    Skip it    ┌─────────────────────────────────┐
               │ 1. ZAP-OUT old position to SOL  │
               │    POST /position/decrease-tx    │
               │    POST /position/landing-...    │
               │                                  │
               │ 2. Check SOL balance             │
               │    GET /token/balance             │
               │                                  │
               │ 3. ZAP-IN new position           │
               │    POST /pools/{id}/add-tx       │
               │    POST /pools/landing-add-tx    │
               └─────────────────────────────────┘
```

### Key Decisions the Bot Makes

| Decision                          | How It's Handled                                               |
| --------------------------------- | -------------------------------------------------------------- |
| **Should I rebalance?**           | Yes, if `inRange` is `false` for the position                  |
| **How much to reinvest?**         | Checks SOL balance after zap-out, reserves 0.05 SOL for fees   |
| **Where to place the new range?** | Centers around the current active bin, with configurable range |
| **What strategy to use?**         | Configurable: `Spot`, `Curve`, or `BidAsk`                     |

***

## Prerequisites

* An LP Agent API key
* A Solana wallet with SOL for transaction fees
* Node.js >= 18

```bash theme={null}
npm install @solana/web3.js bs58
```

***

## Configuration

Before looking at the code, here are the settings you'll want to tune:

| Setting             | Default        | Description                                                                 |
| ------------------- | -------------- | --------------------------------------------------------------------------- |
| `CHECK_INTERVAL_MS` | `60000`        | How often to check positions (ms). 60s is a good starting point.            |
| `SLIPPAGE_BPS`      | `500`          | Slippage tolerance (500 = 5%)                                               |
| `BIN_RANGE`         | `34`           | Bins on each side of active bin. Wider = less rebalancing, lower APR.       |
| `STRATEGY`          | `Spot`         | Distribution: `Spot` (uniform), `Curve` (concentrated), or `BidAsk` (edges) |
| `ZAP_OUT_OUTPUT`    | `allBaseToken` | Withdraw to SOL so you can easily zap-in again                              |
| `POOL_FILTER`       | `undefined`    | Only rebalance a specific pool, or `undefined` for all                      |

***

## Customization Ideas

These snippets show how to extend the bot. Each one plugs into the rebalance loop.

### Add a Stop-Loss

Exit positions that have lost too much value — don't throw good money after bad:

```typescript theme={null}
const STOP_LOSS_PERCENT = -15; // exit at -15% PnL

if (pnlPercent < STOP_LOSS_PERCENT) {
  log(`  PnL ${pnlPercent}% below stop-loss, closing without re-entry`);
  await zapOut(pos.id);
  continue; // skip zap-in
}
```

### Add Discord Notifications

Get alerted when a rebalance happens:

```typescript theme={null}
async function notify(message: string) {
  await fetch("https://discord.com/api/webhooks/YOUR_WEBHOOK", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content: message }),
  });
}

// In the rebalance flow:
await notify(`Rebalanced ${pair}: new position at bins ${fromBinId}-${toBinId}`);
```

### Dynamic Bin Range

Use wider ranges for volatile pools, tighter for stable ones:

```typescript theme={null}
const binStep = pos.poolInfo?.tickSpacing || 1;
const dynamicRange = binStep > 5 ? 20 : 50; // volatile → wider, stable → tighter
```

### Filter by Pool Type

Only rebalance DLMM positions (skip DAMM V2):

```typescript theme={null}
if (pos.protocol !== "meteora") {
  log("  Skipping non-DLMM position");
  continue;
}
```

***

## Running in Production

### With PM2

```bash theme={null}
npm install -g pm2
pm2 start bot.ts --name "lp-rebalance-bot" --interpreter ts-node
pm2 logs lp-rebalance-bot    # view logs
pm2 restart lp-rebalance-bot # restart
pm2 stop lp-rebalance-bot    # stop
```

### With Docker

```dockerfile theme={null}
FROM node:22-slim
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
CMD ["npx", "ts-node", "bot.ts"]
```

### Environment Variables

For production, **never hardcode secrets**:

```typescript theme={null}
const CONFIG = {
  API_KEY: process.env.LP_AGENT_API_KEY!,
  PRIVATE_KEY: process.env.SOLANA_PRIVATE_KEY!,
  // ...
};
```

***

## API Endpoints Used

| Endpoint                             | Purpose                                  |
| ------------------------------------ | ---------------------------------------- |
| `GET /lp-positions/opening`          | Fetch open positions and check `inRange` |
| `GET /token/balance`                 | Check SOL balance for reinvestment       |
| `GET /pools/{poolId}/info`           | Get active bin for new position range    |
| `POST /position/decrease-tx`         | Generate zap-out transactions            |
| `POST /position/landing-decrease-tx` | Land zap-out transactions via Jito       |
| `POST /pools/{poolId}/add-tx`        | Generate zap-in transactions             |
| `POST /pools/landing-add-tx`         | Land zap-in transactions via Jito        |

## Tips

* **Check interval**: 60s is a good default. Too fast = unnecessary API calls. Too slow = missed fees while out of range.
* **Gas costs**: Each rebalance costs \~0.01-0.03 SOL. Factor this into profitability.
* **Bin range**: Wider range = fewer rebalances but lower fee APR. Find the sweet spot for your pair.
* **Transaction landing**: LP Agent's landing endpoints use Jito bundles — no need to set up your own Jito integration.

***

## Complete Bot Script

Here's the full bot ready to copy-paste and run:

<Accordion title="Full auto-rebalancing bot script">
  ```typescript theme={null}
  import { Keypair, Transaction, VersionedTransaction } from "@solana/web3.js";
  import bs58 from "bs58";

  // ===================== CONFIG =====================
  const CONFIG = {
    API_BASE: "https://api.lpagent.io/open-api/v1",
    API_KEY: "your-api-key-here",
    PRIVATE_KEY: "your-base58-private-key",

    CHECK_INTERVAL_MS: 60_000,
    SLIPPAGE_BPS: 500,
    BIN_RANGE: 34,
    STRATEGY: "Spot" as const,
    ZAP_OUT_OUTPUT: "allBaseToken" as const, // withdraw to SOL for easy re-entry

    POOL_FILTER: undefined as string | undefined,
  };

  const wallet = Keypair.fromSecretKey(bs58.decode(CONFIG.PRIVATE_KEY));
  const OWNER = wallet.publicKey.toBase58();

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

  function log(msg: string) {
    console.log(`[${new Date().toISOString()}] ${msg}`);
  }

  function sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // ===================== ZAP-OUT =====================
  async function zapOut(positionId: string, bps: number = 10000): Promise<void> {
    log(`  Zap-Out: withdrawing ${bps / 100}% of position to SOL...`);

    const decreaseTx = await apiCall("POST", "/position/decrease-tx", {
      position_id: positionId,
      bps,
      owner: OWNER,
      slippage_bps: CONFIG.SLIPPAGE_BPS,
      output: CONFIG.ZAP_OUT_OUTPUT,
    });

    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,
    });

    log(`  Zap-Out complete: ${result.data?.signature || "success"}`);
  }

  // ===================== ZAP-IN =====================
  async function zapIn(poolAddress: string, amountSOL: number): Promise<string> {
    log(`  Zap-In: adding ${amountSOL} SOL to pool ${poolAddress}...`);

    const info = await apiCall("GET", `/pools/${poolAddress}/info`);
    const activeBin = info.data.liquidityViz?.activeBin;

    if (!activeBin) {
      throw new Error("Could not get active bin for pool");
    }

    const fromBinId = activeBin.binId - CONFIG.BIN_RANGE;
    const toBinId = activeBin.binId + CONFIG.BIN_RANGE;
    log(`  New range: bin ${fromBinId} to ${toBinId} (active: ${activeBin.binId})`);

    const addTx = await apiCall("POST", `/pools/${poolAddress}/add-tx`, {
      stratergy: CONFIG.STRATEGY,
      inputSOL: amountSOL,
      percentX: 0.5,
      fromBinId,
      toBinId,
      owner: OWNER,
      slippage_bps: CONFIG.SLIPPAGE_BPS,
      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,
    });

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

  // ===================== REBALANCE LOGIC =====================
  async function checkAndRebalance() {
    log("Checking positions...");

    const positionsRes = await apiCall("GET", `/lp-positions/opening?owner=${OWNER}`);
    const positions = positionsRes.data;

    if (!positions || positions.length === 0) {
      log("No open positions found");
      return;
    }

    log(`Found ${positions.length} open position(s)`);

    for (const pos of positions) {
      if (CONFIG.POOL_FILTER && pos.pool !== CONFIG.POOL_FILTER) continue;

      const pair = pos.pairName || `${pos.token0Info?.token_symbol}/${pos.token1Info?.token_symbol}`;
      const value = parseFloat(pos.currentValue) || 0;
      const pnlPercent = pos.pnl?.percent || 0;

      log(`Position: ${pair} | Value: $${value.toFixed(2)} | PnL: ${pnlPercent.toFixed(2)}% | In Range: ${pos.inRange}`);

      if (pos.inRange) {
        log("  In range, skipping");
        continue;
      }

      log("  OUT OF RANGE — rebalancing...");

      try {
        // 1. Zap-Out to SOL
        await zapOut(pos.id);
        await sleep(2000);

        // 2. Check available SOL
        const balanceRes = await apiCall("GET", `/token/balance?owner=${OWNER}`);
        const solBalance = balanceRes.data?.find(
          (t: any) => t.address === "So11111111111111111111111111111111111111112"
        );

        const availableSOL = solBalance ? parseFloat(solBalance.uiAmount) : 0;
        const reserveSOL = 0.05;
        const reinvestSOL = Math.max(0, value / (solBalance?.price || 150) * 0.95);
        const zapInAmount = Math.min(reinvestSOL, availableSOL - reserveSOL);

        if (zapInAmount <= 0.001) {
          log("  Not enough SOL to re-enter, skipping zap-in");
          continue;
        }

        // 3. Zap-In with new range
        log(`  Re-entering with ${zapInAmount.toFixed(4)} SOL...`);
        const newPosition = await zapIn(pos.pool, zapInAmount);
        log(`  Rebalance complete! New position: ${newPosition}`);
      } catch (error: any) {
        log(`  ERROR: ${error.message}`);
      }
    }
  }

  // ===================== BOT LOOP =====================
  async function main() {
    log("=== LP Agent Auto-Rebalance Bot ===");
    log(`Owner: ${OWNER}`);
    log(`Check interval: ${CONFIG.CHECK_INTERVAL_MS / 1000}s`);
    log(`Bin range: +/- ${CONFIG.BIN_RANGE} bins`);
    log(`Strategy: ${CONFIG.STRATEGY}`);
    log(`Pool filter: ${CONFIG.POOL_FILTER || "all pools"}`);
    log("");

    while (true) {
      try {
        await checkAndRebalance();
      } catch (error: any) {
        log(`ERROR: ${error.message}`);
      }

      log(`Next check in ${CONFIG.CHECK_INTERVAL_MS / 1000}s...\n`);
      await sleep(CONFIG.CHECK_INTERVAL_MS);
    }
  }

  main().catch(console.error);
  ```
</Accordion>

## Next Steps

* [Zap-In & Zap-Out Tutorial](/tutorials/add-liquidity-api) — Understand the underlying API flows
* [Copy LP Best Practices](/tutorials/copy-lp-best-practices) — Learn from top LPers
* Explore the full [API Reference](/api-reference/introduction)
