Type/to search
Built-in Functions
Structures
Built-in Variables and Constants
Conventions
Exchanges
Capability Matrix
Centralized Exchanges
Decentralized Exchanges (DEX)
Hyperliquid
Lighter
edgeX
Aster
Aevo
Pendle
WOOFi Pro
Uniswap / PancakeSwap
Aerodrome / Velodrome
KyberSwap
1inch
EVM DEX Common Commands
Jupiter
OKX DEX
DEX Strategy Examples
LP Auto-Rebalancing
Gauge Staking Auto-Compound
DEX-CEX Arbitrage
Jupiter Limit Order Grid
DEX DCA
pump.fun Token Sniping Strategy
Prediction Markets
Securities and Futures
On-chain & Custom

Selects a pool on Aerodrome or Velodrome by reward APR, opens a position, stakes it in the pool's gauge to earn AERO or VELO rewards, then periodically claims the rewards, swaps them to USDC and adds the proceeds back to the position (compounding).

How It Works
  1. Pool selection: from exchange.IO("lp.pools"), keep pools whose gauge is still emitting rewards (gaugeAlive), that are not empty, whose TVL is at least MIN_TVL_USD and whose type matches POOL_KIND; sort them by reward APR rewardAprPct in descending order and take the first. The top 5 candidates are written to the log, with their reward APR and the fee APR they would earn unstaked. When POOL_KIND is slipstream, a Slipstream concentrated liquidity pool is used (range ±RANGE_PCT%); when it is basic, a basic pool is used (full range).
  2. Open and stake: pass stake: true when calling exchange.IO("lp.plan"), so that once exchange.IO("lp.add") has opened the position, it is staked in the gauge directly.
  3. Compounding: check every CLAIM_HOURS hours. When pending rewards are at least MIN_CLAIM_USD USD and exceed twice the gas cost of one compounding round, use exchange.IO("lp.claim") to claim the rewards and swap them to USDC (lp.claim(ids, true)). After the sell order is confirmed on-chain and the proceeds are known, add them to the position: for a basic pool use lp.increase(池子, 金额, {stake: true}); a staked Slipstream position cannot be increased directly, so run exchange.IO("lp.unstake") → exchange.IO("lp.increase") → exchange.IO("lp.stake") in sequence.
  4. Status bar: shows the pool's reward APR and daily emissions (read each round via exchange.IO("lp.pool")), whether the position is staked and in range, position value, pending rewards, cumulative claimed and compounded amounts, the gas estimate, and the annualized return based on rewards actually claimed.

Exchange Object

One Aerodrome / Velodrome exchange object: Aerodrome (Base chain, rewards in AERO) or Velodrome (OP Mainnet, rewards in VELO). Use a dedicated wallet funded with USDC and a small amount of ETH for gas.

Parameters
ParameterDefaultDescription
SYMBOLETH_USDCTrading pair
BUDGET100Position budget (USDC)
POOL_KINDslipstreamslipstream or basic
RANGE_PCT10Slipstream range half-width (%); a staked position earns no rewards once out of range, so a wider range is recommended
MIN_TVL_USD100000Only pools with TVL at or above this value are considered
CLAIM_HOURS24Interval between claiming and compounding (hours)
MIN_CLAIM_USD2Rewards are not claimed while pending rewards are below this value (USD)
GAS_USD0.05Estimated gas per on-chain transaction (USD)
COMPOUND_TXS5Transactions per compounding round: claim, sell, unstake, increase, stake
INTERVAL300Check interval (seconds)
DRY_RUNtrueWhen true, only plans and simulates without sending transactions

Risks
  • The reward APR rewardAprPct is an instantaneous value extrapolated from the current per-second emissions, and it changes with the reward token price, emissions and total staked amount. For Slipstream pools it is calculated from the staked liquidity at the tick of the current price, so it jumps whenever the price moves and can differ several-fold within minutes; when choosing a pool, it is best to check it several times over a period of time. AERO and VELO prices are highly volatile, so the timing of sales affects actual returns.
  • While staked, trading fees go to voters and the position earns only rewards. A Slipstream position earns no rewards once out of range and is also exposed to impermanent loss; a basic pool covers the full range, with impermanent loss following the constant product curve.
  • Every claim, sell, unstake, increase and stake is an on-chain transaction (about COMPOUND_TXS per compounding round), so paying gas is not worthwhile when rewards are too small.

DRY_RUN vs. Live Trading

When DRY_RUN is true (the default), no transactions are sent: the position is simulated with exchange.IO("lp.simulate") (an error from an insufficient wallet balance is expected), and the paper position accrues rewards at the pool's current rewardAprPct (assuming it always stays in range; the current value is taken each round, which amounts to a time-weighted average). When due, a quote is obtained with exchange.IO("route"), and the amount of USDC the rewards would swap to is added to the paper principal. Set DRY_RUN to false to switch to live trading, and adjust GAS_USD for the chain in use.

Examples

Complete strategy code (DRY_RUN = true: plan and simulate only, no transactions are sent).

javascript
/* * Aerodrome / Velodrome gauge staking auto-compounder (example strategy) * * Liquidity positions on Aerodrome (Base) and Velodrome (OP Mainnet) can be staked in the pool's gauge to earn AERO / VELO * emissions (while staked, trading fees go to voters and the position earns only the rewards). This strategy: * 1. Picks from lp.pools, by reward APR (rewardAprPct), a pool whose gauge is still emitting rewards and whose TVL is sufficient: * POOL_KIND = "slipstream" uses a Slipstream concentrated liquidity pool (±RANGE_PCT% range), "basic" uses a basic pool (full range); * 2. Opens a position and stakes it right away (stake: true in lp.plan → lp.add); * 3. Every CLAIM_HOURS hours, if the rewards are worth enough, claims them and sells them for USDC (lp.claim(ids, true)), then adds the proceeds back to the position (compounding): * a staked Slipstream position cannot be increased directly, so it runs lp.unstake → lp.increase → lp.stake; for a basic pool, lp.increase(pool, amount, {stake: true}); * 4. The status bar shows the pool's reward APR, position value, pending rewards, total claimed and compounded amounts, and the annualized yield based on rewards actually claimed. * * DRY_RUN = true (default) sends no transactions: lp.simulate simulates opening the position (an error is expected when the wallet balance is insufficient), and a paper position accrues rewards at * the pool's current rewardAprPct (assuming the price always stays in range); when due, IO("route") fetches a quote to work out how much USDC selling the rewards would yield, * which is added to the paper principal (compounding). * * Risks: * - Reward APR changes with the reward token price, emissions and total stake; rewardAprPct is only an instantaneous value extrapolated from the current per-second emissions; for Slipstream pools it * is computed from the staked liquidity at the current price's tick, so it jumps as soon as the price moves and can differ several-fold within minutes (the paper accrual samples it once per loop, effectively a time average); * AERO / VELO prices are volatile, and the timing of sales affects the actual return; * - A Slipstream position earns no rewards once out of range and is exposed to impermanent loss; a basic pool is full range, with impermanent loss following the constant-product curve; * - Every claim, sell, unstake, increase and stake is an on-chain transaction (about COMPOUND_TXS per compounding round); when rewards are small, gas costs outweigh them. * * Usage: add an Aerodrome or Velodrome exchange object (a dedicated wallet is recommended), and fund the wallet with USDC plus a little ETH for gas. */ // ---------------- Parameters ---------------- var SYMBOL = "ETH_USDC" // trading pair var BUDGET = 100 // position budget (USDC) var POOL_KIND = "slipstream" // slipstream or basic var RANGE_PCT = 10 // Slipstream range half-width (%); a staked position earns no rewards once out of range, so keep it wide var MIN_TVL_USD = 100000 // only consider pools with TVL at or above this var CLAIM_HOURS = 24 // claim + compound interval (hours) var MIN_CLAIM_USD = 2 // do not claim while pending rewards are below this var GAS_USD = 0.05 // estimated gas per on-chain transaction (USD) var COMPOUND_TXS = 5 // transactions per compounding round: claim, sell, unstake, increase, stake var INTERVAL = 300 // check interval (seconds) var DRY_RUN = true // when true, plan and simulate only, no transactions are sent var KEY = DRY_RUN ? "gauge_compound_paper" : "gauge_compound" var S = null var REWARD = null function fmt(x, d) { return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d) } function kindOf(p) { return /^slipstream/.test(p.protocol) ? "slipstream" : "basic" } function rewardPrice() { var t = exchange.GetTicker(REWARD + "_USDC") if (!t) throw new Error("GetTicker " + REWARD + "_USDC failed: " + GetLastError()) return (t.Buy + t.Sell) / 2 } function listPools() { var pair = SYMBOL.split("_") var pools = exchange.IO("lp.pools", pair[0], pair[1]) if (!pools) throw new Error("lp.pools failed: " + GetLastError()) return pools } // Current pool state (the gauge fields of lp.pool use the same calculation as lp.pools) function poolRow(id) { var row = exchange.IO("lp.pool", id) if (!row) throw new Error("lp.pool failed: " + GetLastError()) return row } // Pick the pool by reward APR and list the candidates in the log function choosePool() { var ok = listPools().filter(function (p) { return p.symbol === SYMBOL && p.gaugeAlive && !p.empty && p.tvlUsd >= MIN_TVL_USD && kindOf(p) === POOL_KIND }).sort(function (a, b) { return b.rewardAprPct - a.rewardAprPct }) ok.slice(0, 5).forEach(function (p) { Log("Candidate", p.protocol, p.id, "tick spacing", p.tickSpacing, "TVL $" + fmt(p.tvlUsd, 0), "staked share", fmt(p.stakedShare * 100, 1) + "%", "reward APR", fmt(p.rewardAprPct, 2) + "%", "fee APR (unstaked)", fmt(p.fees24hUsd * 365 / p.tvlUsd * 100, 2) + "%") }) if (!ok.length) throw new Error("No eligible " + POOL_KIND + " pool (TVL ≥ " + MIN_TVL_USD + ", gauge alive)") return ok[0] } function enter(pool) { var params = { symbol: SYMBOL, budget: BUDGET, pool: pool.id, stake: true } if (POOL_KIND === "slipstream") { params.range = { pct: [-RANGE_PCT, RANGE_PCT] }; params.shape = "spot" } var plan = exchange.IO("lp.plan", params) if (!plan) throw new Error("lp.plan failed: " + GetLastError()) Log("Entry plan:", plan.protocol, plan.pool, "needs", fmt(plan.needBase, 6), "+", fmt(plan.needQuote, 4), plan.shortfall ? "(" + plan.shortfall + ")" : "") if (DRY_RUN) { var sim = exchange.IO("lp.simulate", plan) Log(sim ? "lp.simulate passed" : "lp.simulate failed (expected when the wallet balance is insufficient): " + GetLastError()) return ["paper"] } var r = exchange.IO("lp.add", plan) if (!r) throw new Error("lp.add failed: " + GetLastError()) var ids = r.positions ? r.positions.map(function (p) { return String(p.id) }) : [String(r.id)] Log("Position opened and staked", r.txHash, "positions", ids.join(","), r.stake ? "" : "(stake result missing, please check)", "#00AA00") exchange.IO("lp.track", ids) return ids } // Current position: {value, rewards (reward token amount), rewardsValue, staked, inRange} function readPosition(pool) { if (DRY_RUN) { var now = Date.now() S.paperRewardsUsd += S.principal * (pool.rewardAprPct || 0) / 100 * (now - S.lastCheck) / (365 * 86400000) S.lastCheck = now var px = rewardPrice() return { value: S.principal, rewards: S.paperRewardsUsd / px, rewardsValue: S.paperRewardsUsd, staked: true, inRange: true } } var ps = exchange.IO("lp.positions", SYMBOL) if (!ps) throw new Error("lp.positions failed: " + GetLastError()) var out = { value: 0, rewards: 0, rewardsValue: 0, staked: true, inRange: false } ps.filter(function (p) { return !p.closed && S.ids.indexOf(String(p.id)) >= 0 }).forEach(function (p) { out.value += p.value; out.rewards += p.rewards; out.rewardsValue += p.rewardsValue if (!p.staked) out.staked = false if (p.inRange !== false) out.inRange = true // basic pools have no range }) return out } // Wait for the reward sell order to land on-chain and return the USDC actually received (the sell order is a market swap; GetOrder returns the fill from the receipt) function proceeds(orderId) { for (var i = 0; i < 30; i++) { var o = exchange.GetOrder(orderId) if (o && o.Status === ORDER_STATE_CLOSED) return o.DealAmount * o.AvgPrice if (o && o.Status === ORDER_STATE_CANCELED) return 0 Sleep(2000) } return 0 } // Claim rewards → sell for USDC → add back to the position function compound(cur) { Log("Claiming", fmt(cur.rewards, 4), REWARD, "≈ $" + fmt(cur.rewardsValue, 4), "and compounding") var got if (DRY_RUN) { var q = exchange.IO("route", REWARD + "_USDC", "sell", Number(cur.rewards.toFixed(6))) if (!q || q.quote === null) { Log("Quote failed:", GetLastError()); return false } got = q.quote Log("DRY_RUN: selling would yield", fmt(got, 4), "USDC (route", q.best + "); it would be added back to the position via lp.unstake → lp.increase → lp.stake") S.paperRewardsUsd = 0 S.principal += got } else { var r = exchange.IO("lp.claim", S.ids, true) if (!r) { Log("Claim failed:", GetLastError(), "#FF0000"); return false } S.claimed += r.rewards S.lastClaim = Date.now() if (!r.sell || r.sell.error) { Log("Rewards claimed, but the sale failed:", r.sell ? r.sell.error : "-", "#FF0000"); return true } got = proceeds(r.sell.txHash) if (got <= 1) { Log("Sale proceeds of", fmt(got, 4), "USDC are too small, not compounding"); return true } var ok if (POOL_KIND === "basic") { ok = exchange.IO("lp.increase", S.ids[0], got, { stake: true }) } else { ok = exchange.IO("lp.unstake", S.ids) && exchange.IO("lp.increase", S.ids[0], got) && exchange.IO("lp.stake", S.ids) } if (!ok) { Log("Compounding incomplete:", GetLastError(), "(the USDC stays in the wallet; the position may be left unstaked)", "#FF0000"); return true } } S.claimedUsd += got S.compounded += got S.gasUsd += COMPOUND_TXS * GAS_USD S.lastClaim = Date.now() return true } function main() { var name = exchange.GetName() if (name !== "Aerodrome" && name !== "Velodrome") throw new Error("Please add an Aerodrome or Velodrome exchange object; the current one is " + name) REWARD = name === "Aerodrome" ? "AERO" : "VELO" S = _G(KEY) if (!S || S.symbol !== SYMBOL || S.dex !== name) { var pool = choosePool() S = { symbol: SYMBOL, dex: name, pool: pool.id, ids: [], principal: BUDGET, paperRewardsUsd: 0, claimed: 0, claimedUsd: 0, compounded: 0, gasUsd: 0, start: Date.now(), lastClaim: Date.now(), lastCheck: Date.now() } S.ids = enter(pool) S.gasUsd += 3 * GAS_USD _G(KEY, S) } else { Log("Restored from _G: pool", S.pool, "positions", S.ids.join(",")) if (!DRY_RUN) exchange.IO("lp.track", S.ids) } while (true) { try { var pool = poolRow(S.pool) var cur = readPosition(pool) var due = Date.now() - S.lastClaim >= CLAIM_HOURS * 3600000 if (due && cur.rewardsValue >= MIN_CLAIM_USD && cur.rewardsValue > 2 * COMPOUND_TXS * GAS_USD && compound(cur)) { cur.rewards = 0; cur.rewardsValue = 0 } _G(KEY, S) var days = Math.max((Date.now() - S.start) / 86400000, 1e-6) var realized = (S.claimedUsd + cur.rewardsValue) / BUDGET / days * 365 * 100 var table = { type: "table", title: SYMBOL + " gauge staking compounder (" + REWARD + ")" + (DRY_RUN ? " (DRY_RUN paper position)" : ""), cols: ["Item", "Value"], rows: [ ["Pool", pool.protocol + " " + S.pool], ["Pool reward APR rewardAprPct", fmt(pool.rewardAprPct, 2) + "%"], ["Daily emissions", fmt(pool.rewardsPerDay, 0) + " " + REWARD + " ($" + fmt(pool.rewardsPerDayUsd, 0) + ")"], ["Positions", S.ids.join(",")], ["Staked / in range", (cur.staked ? "Yes" : "No") + " / " + (cur.inRange ? "Yes" : "No")], ["Position value", fmt(cur.value, 4)], ["Pending rewards", fmt(cur.rewards, 4) + " " + REWARD + " ($" + fmt(cur.rewardsValue, 4) + ")"], ["Total claimed / compounded (USDC)", fmt(S.claimedUsd, 4) + " / " + fmt(S.compounded, 4)], ["Estimated gas", fmt(S.gasUsd, 4)], ["Annualized yield from actual rewards", fmt(realized, 2) + "%"], ["Next claim", due ? "Due (waiting for rewards ≥ $" + MIN_CLAIM_USD + ")" : _D(S.lastClaim + CLAIM_HOURS * 3600000)]] } LogStatus(_D(), "\n`" + JSON.stringify(table) + "`") } catch (e) { Log("Error in this round:", e.message || e, "#FF0000") } Sleep(INTERVAL * 1000) } }

See Also