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",
// Bot settings
CHECK_INTERVAL_MS: 60_000, // Check every 60 seconds
SLIPPAGE_BPS: 500, // 5% slippage tolerance
BIN_RANGE: 34, // Bins on each side of active bin
STRATEGY: "Spot" as const, // "Spot" | "Curve" | "BidAsk"
ZAP_OUT_OUTPUT: "both" as const,
// Optional filters — only rebalance positions matching these
POOL_FILTER: undefined as string | undefined, // specific pool address, or undefined for all
};
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...`);
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);
// Land via LP Agent's Jito integration (better landing rate than direct RPC — free!)
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}...`);
// Get current active bin
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})`);
// Generate zap-in transactions
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",
});
// Sign and land via Jito
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) {
// Skip if pool filter is set and doesn't match
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(" Position is in range, skipping");
continue;
}
log(" Position is OUT OF RANGE — rebalancing...");
try {
// Step 1: Zap-Out (close the old position)
await zapOut(pos.id);
// Brief pause to let state settle
await sleep(2000);
// Step 2: Estimate how much SOL we recovered
// Fetch the SOL balance to use for the new position
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;
// Keep some SOL for transaction fees, use the rest
const reserveSOL = 0.05;
const reinvestSOL = Math.max(0, value / (solBalance?.price || 150) * 0.95); // rough estimate
const zapInAmount = Math.min(reinvestSOL, availableSOL - reserveSOL);
if (zapInAmount <= 0.001) {
log(" Not enough SOL to re-enter position, skipping zap-in");
continue;
}
log(` Re-entering with ${zapInAmount.toFixed(4)} SOL...`);
// Step 3: Zap-In (open new position)
const newPosition = await zapIn(pos.pool, zapInAmount);
log(` Rebalance complete! New position: ${newPosition}`);
} catch (error: any) {
log(` ERROR during rebalance: ${error.message}`);
// Continue to next position instead of crashing
}
}
}
// ===================== 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 in check loop: ${error.message}`);
}
log(`Next check in ${CONFIG.CHECK_INTERVAL_MS / 1000}s...\n`);
await sleep(CONFIG.CHECK_INTERVAL_MS);
}
}
main().catch(console.error);