I was logging into Raydium every single morning, clicking “Harvest,” swapping half the rewards on Jupiter, adding liquidity, and staking the LP tokens again. Every. Single. Day.
It took about 10 minutes each time. Not terrible, but I missed days. Weekends especially. And every missed day meant rewards sitting there doing nothing instead of compounding. So I wrote a script to do it all for me.
How Solana Yield Farming Works (Quick Version)
You deposit two tokens into a liquidity pool — say SOL and USDC. The pool gives you LP tokens representing your share. You stake those LP tokens in a farm, and you earn rewards (usually RAY or some other token) on top of the trading fees.
The catch? Those rewards just pile up. They don’t compound automatically. If you want compound growth, you have to manually harvest, swap, re-add liquidity, and re-stake. That’s the part I automated.
Checking Your LP Position and Pending Rewards
First thing the script does is check what’s sitting in the farm. You need to read the on-chain account data for your staking position.
Here’s what I do:
import { Connection, PublicKey } from "@solana/web3.js";
import { Farm } from "@raydium-io/raydium-sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const walletPubkey = new PublicKey("YOUR_WALLET_ADDRESS");
async function getPendingRewards(farmId: string) {
const farmPubkey = new PublicKey(farmId);
const farmState = await Farm.fetchFarmState(connection, farmPubkey);
const userStakeAccounts = await Farm.fetchAllStakeAccountsByOwner(
connection,
walletPubkey
);
const myStake = userStakeAccounts.find(
(s) => s.farmId.toBase58() === farmId
);
if (!myStake) {
console.log("No stake found for this farm");
return null;
}
const pendingReward = Farm.calcPendingReward(farmState, myStake);
console.log(`Pending rewards: ${pendingReward.toNumber() / 1e6} RAY`);
return pendingReward;
}
I used to skip this check and just blindly harvest. Bad idea. If your pending rewards are tiny, the transaction fees eat into your gains. Now I only compound when the rewards cross a minimum threshold.
Harvesting Rewards from Raydium
Once the rewards are worth grabbing, you send a harvest transaction. This pulls your earned tokens into your wallet without unstaking your LP.
import { Keypair, Transaction } from "@solana/web3.js";
import { Farm } from "@raydium-io/raydium-sdk";
async function harvestRewards(farmId: string, wallet: Keypair) {
const farmPubkey = new PublicKey(farmId);
const { transaction, signers } = await Farm.makeHarvestTransaction({
connection,
farmId: farmPubkey,
userKeys: {
tokenAccounts: await getTokenAccounts(connection, wallet.publicKey),
owner: wallet.publicKey,
},
});
transaction.feePayer = wallet.publicKey;
transaction.recentBlockhash = (
await connection.getLatestBlockhash()
).blockhash;
transaction.sign(wallet, ...signers);
const txId = await connection.sendRawTransaction(transaction.serialize());
await connection.confirmTransaction(txId);
console.log(`Harvested. TX: ${txId}`);
return txId;
}
Nothing fancy. The Raydium SDK handles the instruction building. You just sign and send.
Swapping Rewards to LP Tokens via Jupiter
This is where it gets interesting. After harvesting, you’ve got RAY tokens (or whatever the reward is). You need to swap half of them into each side of the LP pair.
I use Jupiter’s API for this because it finds the best route automatically.
async function swapViaJupiter(
inputMint: string,
outputMint: string,
amountLamports: number,
wallet: Keypair
) {
const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amountLamports}&slippageBps=50`;
const quoteResponse = await fetch(quoteUrl).then((r) => r.json());
const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
quoteResponse,
userPublicKey: wallet.publicKey.toBase58(),
wrapAndUnwrapSol: true,
}),
}).then((r) => r.json());
const swapTransaction = Buffer.from(swapResponse.swapTransaction, "base64");
const transaction = Transaction.from(swapTransaction);
transaction.sign(wallet);
const txId = await connection.sendRawTransaction(transaction.serialize());
await connection.confirmTransaction(txId);
console.log(`Swapped. TX: ${txId}`);
return txId;
}
I set slippage to 50 bps (0.5%). For stable pairs like USDC/USDT, you can go lower. For volatile pairs, I’ve had 50 bps fail on me during big moves, so keep an eye on that.
Re-depositing Into the Farm
After the swap, you’ve got both sides of the pair. Add liquidity, get LP tokens, stake them back. That’s the compound.
async function addLiquidityAndStake(
poolId: string,
farmId: string,
wallet: Keypair,
tokenAAmount: number,
tokenBAmount: number
) {
const poolPubkey = new PublicKey(poolId);
const { transaction: lpTx, signers: lpSigners } =
await Liquidity.makeAddLiquidityTransaction({
connection,
poolId: poolPubkey,
userKeys: {
tokenAccounts: await getTokenAccounts(connection, wallet.publicKey),
owner: wallet.publicKey,
},
amountInA: tokenAAmount,
amountInB: tokenBAmount,
fixedSide: "a",
});
lpTx.feePayer = wallet.publicKey;
lpTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
lpTx.sign(wallet, ...lpSigners);
const lpTxId = await connection.sendRawTransaction(lpTx.serialize());
await connection.confirmTransaction(lpTxId);
console.log(`Liquidity added. TX: ${lpTxId}`);
// Now stake the new LP tokens
const { transaction: stakeTx, signers: stakeSigners } =
await Farm.makeDepositTransaction({
connection,
farmId: new PublicKey(farmId),
userKeys: {
tokenAccounts: await getTokenAccounts(connection, wallet.publicKey),
owner: wallet.publicKey,
},
amount: await getLpTokenBalance(connection, wallet.publicKey, poolId),
});
stakeTx.feePayer = wallet.publicKey;
stakeTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
stakeTx.sign(wallet, ...stakeSigners);
const stakeTxId = await connection.sendRawTransaction(stakeTx.serialize());
await connection.confirmTransaction(stakeTxId);
console.log(`Staked. TX: ${stakeTxId}`);
}
The Rebalancing Check
Here’s something I learned the hard way. If one token in your pair moves 20% against the other, just blindly adding liquidity at the current ratio wastes money. You end up providing liquidity at a skewed price and eating unnecessary impermanent loss.
Before re-depositing, I check the ratio:
async function shouldRebalance(
poolId: string,
targetRatio: number,
threshold: number
) {
const poolState = await Liquidity.fetchPoolState(
connection,
new PublicKey(poolId)
);
const currentRatio =
poolState.baseReserve.toNumber() / poolState.quoteReserve.toNumber();
const drift = Math.abs(currentRatio - targetRatio) / targetRatio;
console.log(`Current ratio: ${currentRatio}, drift: ${(drift * 100).toFixed(2)}%`);
if (drift > threshold) {
console.log("Drift too high — skipping compound, consider rebalancing");
return true;
}
return false;
}
I use a 5% drift threshold. If the pool ratio has moved more than 5% from when I entered, I skip the auto-compound and log a warning. Some people auto-rebalance, but I prefer to review those manually. Rebalancing locks in losses, and sometimes it’s better to just wait.
The Cron Runner
Everything ties together in a single function that runs on a schedule:
const FARM_ID = "YOUR_FARM_ID";
const POOL_ID = "YOUR_POOL_ID";
const RAY_MINT = "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R";
const SOL_MINT = "So11111111111111111111111111111111111111112";
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const MIN_REWARD_THRESHOLD = 1_000_000; // 1 RAY in lamports
async function compoundLoop(wallet: Keypair) {
console.log(`[${new Date().toISOString()}] Starting compound cycle...`);
const pending = await getPendingRewards(FARM_ID);
if (!pending || pending.toNumber() < MIN_REWARD_THRESHOLD) {
console.log("Rewards below threshold. Skipping.");
return;
}
const needsRebalance = await shouldRebalance(POOL_ID, 1.0, 0.05);
if (needsRebalance) {
console.log("Pool drifted too far. Skipping auto-compound.");
return;
}
await harvestRewards(FARM_ID, wallet);
// Wait a moment for the harvest TX to settle
await new Promise((r) => setTimeout(r, 3000));
const rewardBalance = await getTokenBalance(connection, wallet.publicKey, RAY_MINT);
const halfReward = Math.floor(rewardBalance / 2);
// Swap half rewards to SOL, half to USDC (for SOL-USDC pool)
await swapViaJupiter(RAY_MINT, SOL_MINT, halfReward, wallet);
await swapViaJupiter(RAY_MINT, USDC_MINT, halfReward, wallet);
await new Promise((r) => setTimeout(r, 3000));
const solBalance = await getTokenBalance(connection, wallet.publicKey, SOL_MINT);
const usdcBalance = await getTokenBalance(connection, wallet.publicKey, USDC_MINT);
await addLiquidityAndStake(POOL_ID, FARM_ID, wallet, solBalance, usdcBalance);
console.log("Compound complete.");
}
// Run every 6 hours
setInterval(() => {
compoundLoop(wallet).catch(console.error);
}, 6 * 60 * 60 * 1000);
compoundLoop(wallet).catch(console.error);
I run this every 6 hours. For most pools, that’s the sweet spot — frequent enough to capture compound gains, infrequent enough that tx fees don’t eat you alive.
When Auto-Compounding Helps vs Hurts
Let me be blunt: auto-compounding doesn’t fix a bad pool.
If you’re in a pool where one token is dumping 10% a week, compounding just means you’re buying more of a falling asset faster. Impermanent loss will destroy you regardless of how often you compound.
Auto-compounding works best when:
- Both tokens are relatively stable (stablecoin pairs, or correlated assets like mSOL/SOL)
- Farm APR is high enough that compound interest actually matters. Below 30% APR, compounding every 6 hours barely beats daily.
- Trading volume in the pool is real, not just wash trading that’ll disappear next week
I’ve found the best results with SOL-USDC on Raydium and stablecoin pairs. The boring stuff. High-APR meme token farms look amazing on paper, but I’ve been burned enough times to know better.
Picking the Right Pools
Here’s my checklist before I farm anything:
- Daily volume over $1M. Below that, the fees you earn are negligible.
- TVL above $5M. Thin pools are manipulation magnets.
- Reward token isn’t pure garbage. If the farm reward is some random token with no utility, you’re just getting paid in monopoly money.
- The pair makes sense. SOL-USDC? Great. Random-token-A vs Random-token-B? Pass.
I used to chase the highest APR number on the page. Don’t do that. A 500% APR farm that lasts two weeks and dumps the reward token 80% gives you less than a boring 40% APR stable farm that runs for months.
Keep Your Keys Safe
One thing I want to be clear about: this script needs your private key to sign transactions. Never put your main wallet’s private key in a script. I use a dedicated farming wallet with only the funds I’m willing to risk. The key is loaded from an environment variable, never hardcoded.
import { Keypair } from "@solana/web3.js";
import bs58 from "bs58";
const wallet = Keypair.fromSecretKey(
bs58.decode(process.env.FARM_WALLET_PRIVATE_KEY!)
);
If the server gets compromised, you lose what’s in that wallet. That’s it. Not your whole portfolio.
Automation saves real time and real money through compounding. I went from spending 10 minutes a day clicking buttons to checking a log file once a week. But the script is the easy part. Picking the right pools is still the hard part, and no amount of automation fixes a bad position. Write the code, but do the research first.