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: 5 * 60_000, // 5 minutes
MIN_FEE_USD: 1, // only claim when >= $1 in fees
SLIPPAGE_BPS: 500,
RESERVE_SOL: 0.05, // keep this much SOL in wallet
STRATEGY: "Spot" as const,
BIN_RANGE: 34,
POOL_FILTER: undefined as string | undefined, // or a specific pool address
};
const SOL_MINT = "So11111111111111111111111111111111111111112";
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) throw new Error(`API ${method} ${path} failed: ${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");
}
}
const log = (msg: string) => console.log(`[${new Date().toISOString()}] ${msg}`);
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
// ===================== CLAIM + SWAP =====================
async function claimAndSwap(pos: any): Promise<void> {
log(` Claim: requesting claim-fee-tx (swapToNative=true)...`);
const claimRes = await apiCall("POST", "/position/claim-fee-tx", {
position_id: pos.id,
owner: OWNER,
slippage_bps: CONFIG.SLIPPAGE_BPS,
swapToNative: true,
type: pos.protocol === "meteora_damm_v2" ? "meteora_damm_v2" : "meteora",
});
const { lastValidBlockHeight, claimTxsWithJito, swapTxsWithJito } = claimRes.data;
const signedClaim = claimTxsWithJito.map(signTx);
const signedSwap = swapTxsWithJito.map(signTx);
const landRes = await apiCall("POST", "/position/landing-claim-fee-tx", {
lastValidBlockHeight,
claimTxsWithJito: signedClaim,
swapTxsWithJito: signedSwap,
});
log(` Claim landed: ${landRes.data?.signature || "success"}`);
}
// ===================== ZAP-IN (RE-DEPOSIT) =====================
async function reDeposit(poolAddress: string, amountSOL: number): Promise<void> {
log(` Re-deposit: zapping ${amountSOL.toFixed(4)} SOL back into pool...`);
const info = await apiCall("GET", `/pools/${poolAddress}/info`);
const activeBin = info.data.liquidityViz?.activeBin;
if (!activeBin) throw new Error("Could not get active bin");
const fromBinId = activeBin.binId - CONFIG.BIN_RANGE;
const toBinId = activeBin.binId + CONFIG.BIN_RANGE;
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 result = await apiCall("POST", "/pools/landing-add-tx", {
lastValidBlockHeight: addTx.data.lastValidBlockHeight,
swapTxsWithJito: addTx.data.swapTxsWithJito.map(signTx),
addLiquidityTxsWithJito: addTx.data.addLiquidityTxsWithJito.map(signTx),
meta: addTx.data.meta,
});
log(` Re-deposit done: https://solscan.io/tx/${result.data.signature}`);
}
// ===================== COMPOUND LOGIC =====================
async function checkAndCompound() {
log("Checking positions for compoundable fees...");
const positionsRes = await apiCall("GET", `/lp-positions/opening?owner=${OWNER}`);
const positions = positionsRes.data || [];
if (positions.length === 0) {
log("No open positions found");
return;
}
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 uncollectedUsd = parseFloat(pos.uncollectedFee || "0");
log(`Position: ${pair} | Uncollected: $${uncollectedUsd.toFixed(2)} | In Range: ${pos.inRange}`);
if (uncollectedUsd < CONFIG.MIN_FEE_USD) {
log(` Below threshold ($${CONFIG.MIN_FEE_USD}), skipping`);
continue;
}
log(" COMPOUNDING...");
try {
// Snapshot SOL balance before the claim
const before = await apiCall("GET", `/token/balance?owner=${OWNER}`);
const beforeSol = before.data?.find((t: any) => t.address === SOL_MINT);
const beforeAmount = beforeSol ? parseFloat(beforeSol.uiAmount) : 0;
// 1. Claim + swap to SOL in one round-trip
await claimAndSwap(pos);
await sleep(3000);
// 2. Read post-claim balance
const after = await apiCall("GET", `/token/balance?owner=${OWNER}`);
const afterSol = after.data?.find((t: any) => t.address === SOL_MINT);
const afterAmount = afterSol ? parseFloat(afterSol.uiAmount) : 0;
const gained = afterAmount - beforeAmount;
log(` Gained ${gained.toFixed(4)} SOL from claim`);
const depositSOL = Math.max(0, afterAmount - CONFIG.RESERVE_SOL);
if (depositSOL <= 0.001) {
log(" Not enough SOL to re-deposit, skipping");
continue;
}
// 3. Re-deposit into the same pool
await reDeposit(pos.pool, depositSOL);
log(" Compound complete!");
} catch (error: any) {
log(` ERROR: ${error.message}`);
}
}
}
// ===================== BOT LOOP =====================
async function main() {
log("=== LP Agent Auto-Compound Bot ===");
log(`Owner: ${OWNER}`);
log(`Check interval: ${CONFIG.CHECK_INTERVAL_MS / 1000}s`);
log(`Min fee threshold: $${CONFIG.MIN_FEE_USD}`);
log(`Pool filter: ${CONFIG.POOL_FILTER || "all pools"}`);
log("");
while (true) {
try {
await checkAndCompound();
} 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);