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