Type/to search
Built-in Functions
Structures
Built-in Variables and Constants
Conventions
Exchanges
Capability Matrix
Centralized Exchanges
Decentralized Exchanges (DEX)
Prediction Markets
Securities and Futures
On-chain & Custom

Adds liquidity by a plan: swaps first when needed, approves, creates every position of the plan in one transaction and returns the position IDs once it is mined.

exchange.IO(k, plan)

Examples

  • Add a ±5% position in the chosen pool.

    javascript
    function main() { var sel = exchange.IO("lp.select", "ETH", 200, {quote: "USDC"}) var params = {symbol: "ETH_USDC", budget: 200, pool: sel.id, range: {pct: [-5, 5]}} var plan = exchange.IO("lp.plan", params) if (!plan || plan.shortfall) { Log("Cannot add:", plan ? plan.shortfall : GetLastError()) return } var r = exchange.IO("lp.add", plan) if (!r) { Log("Adding failed:", GetLastError()) return } r.positions.forEach(function (p) { Log("Position", p.id, "range", p.priceLower, "~", p.priceUpper, "uses", p.amountBase, "ETH +", p.amountQuote, "USDC") }) }
  • A complete LP strategy: pick a pool, plan, simulate and add; check the positions every minute, collect fees once they are large enough, and withdraw and re-add after the price stays out of range.

    javascript
    // The complete LP flow: pick a pool → plan → simulate → add → monitor → collect fees // → withdraw and re-add after the price leaves the range var SYMBOL = "ETH_USDC" var BUDGET = 200 // budget of each position (USDC) var RANGE = {pct: [-5, 5]} // ±5% around the price var OUT_LIMIT = 30 // withdraw and re-add after this many checks (minutes) out of range var MIN_FEES = 5 // collect once uncollected fees exceed this many USDC function open() { var sel = exchange.IO("lp.select", "ETH", BUDGET, {quote: "USDC"}) if (!sel) { Log("Pool selection failed:", GetLastError()) return null } var plan = exchange.IO("lp.plan", {symbol: SYMBOL, budget: BUDGET, pool: sel.id, range: RANGE}) if (!plan || plan.shortfall) { Log("Cannot add:", plan ? plan.shortfall : GetLastError()) return null } Log("Pool", plan.pool, plan.protocol, "est. daily fees", plan.estDailyFeesUsd, "USD") // a plan that needs a swap cannot be simulated before the swap (lp.add swaps first) if (!plan.swap && !exchange.IO("lp.simulate", plan)) { Log("Simulation failed:", GetLastError()) return null } var r = exchange.IO("lp.add", plan) if (!r) { Log("Adding failed:", GetLastError()) return null } var ids = r.positions.map(function (p) { return p.id }) _G("lpIds", ids) // keep the IDs to find the positions again after a restart Log("Added", ids, "transaction", r.txHash) return ids } function main() { var ids = _G("lpIds") if (ids) { // lets lp.positions find V4/Infinity CL and Aerodrome/Velodrome positions after a restart exchange.IO("lp.track", ids) } var outCount = 0 while (true) { if (!ids) { ids = open() if (!ids) { Sleep(60000) continue } } var mine = (exchange.IO("lp.positions", SYMBOL) || []).filter(function (p) { return ids.indexOf(p.id) >= 0 && !p.closed }) if (mine.length == 0) { ids = null continue } var fees = 0 var inRange = false mine.forEach(function (p) { fees += p.feesValue inRange = inRange || p.inRange }) LogStatus(_D(), "positions", ids, inRange ? "in range" : "out of range", "fees", fees, "USDC") if (fees > MIN_FEES) { exchange.IO("lp.collect", ids) } outCount = inRange ? 0 : outCount + 1 if (outCount >= OUT_LIMIT) { // withdraw everything and sell the returned ETH for USDC; re-add in the next round var r = exchange.IO("lp.remove", ids, 1, true) Log("Withdrawn", r ? r.txHash : GetLastError()) ids = null _G("lpIds", null) outCount = 0 } Sleep(60000) } }

Returns

TypeDescription

object / null value

{txHash, swapTxHash, pool, positions: [{id, tickLower, tickUpper, priceLower, priceUpper, liquidity, amountBase, amountQuote}]}: txHash is the add transaction, swapTxHash the swap before it (null without one), and id in positions the position ID. Null with an error on failure.

Arguments

NameTypeRequiredDescription

k

string

Yes

Set to lp.add to add liquidity.

plan

object

Yes

A plan returned by exchange.IO("lp.plan", ...), or its params; it is recomputed from params at the latest pool price.

See Also

Remarks

Steps: the plan is recomputed at the latest pool price, and a plan with shortfall fails without sending anything → the needed swap goes first as a market order (swapTxHash) → approvals: V3 and Slipstream approve the PositionManager, V4/Infinity CL approve Permit2 and then grant the PositionManager an allowance inside Permit2, following exchange.IO("approve") exact/max → one transaction creates every position, and the call returns once it is mined. If a step after the swap fails, the error says the swap already went through (the swapped tokens stay in the wallet).

Price protection and payment: each position pays at most its planned amounts plus slippage (the plan's slippage, 1% by default, within what the wallet holds), and the whole transaction reverts (costing only gas) when price moves make it use less than the planned amounts × (1 − slippage). In V3 and Slipstream pools with a native pair the wrapped-token side is paid as the plan's payWith says: entirely in the native coin (sent with the transaction, the excess refunded by refundETH) or entirely in the wrapped token; collected amounts later come back as the wrapped token. V4/Infinity CL native pools use and return the native coin. About 3 million gas at the current gas price is kept back from the native coin for fees.

Position IDs: V3 positions are PositionManager NFT numbers (such as 1381183); v4:number on Uniswap V4 and inf:number on PancakeSwap Infinity CL; aero:number (old factory) and aero2:number (new factory) on Aerodrome, velo:number and velo2:number on Velodrome; a basic pool has no NFT and its position ID is the pool address. When the node does not serve the full log history, V4/Infinity CL and Slipstream positions must be registered with exchange.IO("lp.track", ids) after a restart before lp.positions finds them, so keep the returned IDs (for example with _G()).

Write commands return after the transaction is mined, and for the next minute on-chain reads are pinned to no earlier than its block, so lp.positions called right after sees the new positions.

Aerodrome and Velodrome basic pools are added through the Router (using the native balance with a native pair, addLiquidityETH), returning {txHash, swapTxHash, pool, id, lpTokens, amountBase, amountQuote}; with stake true in the plan the result also has stake (as returned by exchange.IO("lp.stake")).

Complete runnable strategies, such as re-centering the range after the price leaves it or gauge staking with compounding on Aerodrome and Velodrome, are in DEX Strategy Examples.