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

Places several levels of native Jupiter limit orders above and below the current price. When an order fills, a counter-order is placed one grid step away to capture a one-step spread. When the price leaves the range, all open orders are canceled and the grid is rebuilt around the new price.

How It Works
  1. Build the grid: buy orders at current price × (1 − k × STEP_PCT%) and sell orders at current price × (1 + k × STEP_PCT%), k = 1…LEVELS. Orders are placed with exchange.CreateOrder(交易对, 方向, 价格, 数量, {tif: "gtc"}). When the price is reached, Jupiter's keeper fills the order, so the strategy does not need to watch the market continuously.
  2. Every INTERVAL seconds, open orders missing from the result of exchange.GetOrders are checked with exchange.GetOrder. For each filled order, a counter-order is placed one grid step away (buy filled → sell one step higher; sell filled → buy one step lower).
  3. When the mid price moves more than ±BAND_PCT% from the grid center, all open orders are canceled and the grid is rebuilt around the new price.
  4. Open order IDs are stored in _G, so tracking continues after a restart. The status bar lists the current open orders and the PnL of filled orders, valued at the current price.

The size of each order, ORDER_USD, must meet Jupiter's minimum order value: at least $5 for Trigger v1 and at least $10 for V2. At startup the API mode is read with exchange.IO("trigger.api"). If it is not v1, the $10 minimum is checked, with a 5% buffer. If a Jupiter API Key is configured and the quote currency is USDC or USDT, the hosted V2 is used by default. See exchange.IO("trigger.*", ...) for details.

Exchange Object

One Jupiter exchange object (use a dedicated wallet). The wallet must hold the base currency (for sell orders), the quote currency (for buy orders), and a small amount of SOL to pay fees.

Parameters
ParameterDefaultDescription
SYMBOLSOL_USDCTrading pair
LEVELS3Number of order levels on each side
STEP_PCT1Grid step (%)
ORDER_USD12Value of each order (in the quote currency, about USD)
BAND_PCT5How far (%) the price can move from the center before the grid is rebuilt
EXPIRY_HOURS0Order expiry (hours). When 0, v1 orders never expire and V2 uses the default of 30 days
INTERVAL60Check interval (seconds)
DRY_RUNtrueWhen true, orders are only simulated and not actually placed

Risks
  • In a one-way market the grid keeps buying (as the price falls) or sells out (as the price rises), and re-centering locks in the unrealized loss.
  • v1 limit orders fill at the swap rate. V2 swaps at market once the USD price triggers, so the fill price is not guaranteed. V2 funds are held in a vault hosted by Jupiter.
  • Every order placement and cancellation is a Solana transaction that pays network and priority fees.

DRY_RUN vs. Live Trading

When DRY_RUN is true (the default), no orders are actually placed. Each order that would be placed is simulated with trigger.simulate (a failed simulation because of low wallet balance is normal, and reason gives the cause). Paper orders count as filled when the market price crosses them (buy order: best ask ≤ order price; sell order: best bid ≥ order price). Set DRY_RUN to false for live trading. Live orders lock up coins in the wallet.

Examples

Complete strategy code (with DRY_RUN = true, orders are only simulated and no real orders are placed).

javascript
/* * Jupiter native limit-order grid (Solana, example strategy) * * Uses Jupiter native limit orders (exchange.CreateOrder(…, {tif: "gtc"}); once the price is reached, Jupiter's keeper fills the order, * so there is no need to watch the market continuously) and places LEVELS levels above and below the current price: buy price = current price × (1 − k × STEP_PCT%), sell price = current price × (1 + k × STEP_PCT%). * - When an order fills, an opposite order is placed one level away to replace it (buy filled → sell one level higher, sell filled → buy one level lower), capturing one grid spread; * - When the price deviates from the center price by more than ±BAND_PCT%, all open orders are canceled and the grid is rebuilt around the new price; * - The per-order amount ORDER_USD must meet Jupiter's minimum order size: at least $5 for Trigger v1 and at least $10 for V2 * (when a Jupiter API Key is configured and the quote currency is USDC / USDT, the custodial V2 is used by default; see "Limit Orders" in the exchange documentation). * Order IDs are saved in _G, so they can still be tracked after a restart. * * With DRY_RUN = true (default), no real orders are placed: every order that "would have been placed" is simulated with IO("trigger.simulate", …) (the simulation returns an error when the wallet balance * is insufficient, which is expected), and paper orders are considered filled when the market crosses their price (buy: best ask ≤ order price; sell: best bid ≥ order price). * * Risks: in a one-sided market the grid keeps buying (when falling) or sells out entirely (when rising), and re-centering turns unrealized losses into realized losses; v1 limit orders fill at the swap ratio, * while V2 orders are triggered by the USD price and then swapped at market, so the fill price is not guaranteed; V2 funds are held in a Jupiter-custodied vault; placing and canceling an order are each a separate * Solana transaction and incur network fees and priority fees. * * Usage: add a Jupiter exchange object (a dedicated wallet is recommended); the wallet must hold the base currency (for sell orders), the quote currency (for buy orders) and a small amount of SOL (for fees). */ // ---------------- Parameters ---------------- var SYMBOL = "SOL_USDC" // Trading pair var LEVELS = 3 // Number of order levels per side var STEP_PCT = 1 // Grid spacing (%) var ORDER_USD = 12 // Per-order amount (in quote currency, roughly USD) var BAND_PCT = 5 // Rebuild the grid when the price deviates from the center price by more than this percentage var EXPIRY_HOURS = 0 // Order validity (hours); 0 = never expires on v1, V2 uses the default 30 days var INTERVAL = 60 // Check interval (seconds) var DRY_RUN = true // When true, only simulate and do not place real orders var KEY = DRY_RUN ? "jup_grid_paper" : "jup_grid" var G = null // {center, seq, orders: {id: {side, price, amount}}, buys, sells, spent, received, bought, sold} function fmt(x, d) { return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d) } function roundPx(p) { return Number(p.toPrecision(6)) } function place(side, price) { price = roundPx(price) var amount = Math.ceil(ORDER_USD / price * 1e6) / 1e6 var opts = { tif: "gtc" } if (EXPIRY_HOURS > 0) opts.expiry = Date.now() + EXPIRY_HOURS * 3600000 var id if (DRY_RUN) { var p = { symbol: SYMBOL, side: side, price: price, amount: amount } if (opts.expiry) p.expiry = opts.expiry var sim = exchange.IO("trigger.simulate", p) Log("Place order", side, amount, "@", price, sim ? (sim.ok ? "simulation passed" : "simulation failed: " + sim.reason) : "simulation error: " + GetLastError()) id = "paper-" + (++G.seq) } else { id = exchange.CreateOrder(SYMBOL, side, price, amount, opts) if (!id) { Log("Order placement failed", side, amount, "@", price, GetLastError(), "#FF0000"); return } Log("Place order", side, amount, "@", price, id) } G.orders[id] = { side: side, price: price, amount: amount } } function buildGrid(price) { G.center = price for (var k = 1; k <= LEVELS; k++) { place("buy", price * (1 - k * STEP_PCT / 100)) place("sell", price * (1 + k * STEP_PCT / 100)) } _G(KEY, G) } function cancelAll() { for (var id in G.orders) { if (!DRY_RUN && !exchange.CancelOrder(id)) Log("Cancel failed", id, GetLastError()) } G.orders = {} } // Find filled orders, returns [id] function filledIds(t) { var out = [] if (DRY_RUN) { for (var id in G.orders) { var o = G.orders[id] if ((o.side === "buy" && t.Sell <= o.price) || (o.side === "sell" && t.Buy >= o.price)) out.push(id) } return out } var open = exchange.GetOrders(SYMBOL) if (!open) throw new Error("GetOrders failed: " + GetLastError()) var openIds = open.map(function (o) { return o.Id }) for (var oid in G.orders) { if (openIds.indexOf(oid) >= 0) continue var r = exchange.GetOrder(oid) if (!r) continue // Not queryable yet, check again next round if (r.Status === ORDER_STATE_CLOSED) out.push(oid) else if (r.Status === ORDER_STATE_CANCELED) { Log("Order canceled", oid); delete G.orders[oid] } } return out } function onFill(id) { var o = G.orders[id] delete G.orders[id] Log("Filled", o.side, o.amount, "@", o.price, "#00AA00") if (o.side === "buy") { G.buys++; G.spent += o.amount * o.price; G.bought += o.amount place("sell", o.price * (1 + STEP_PCT / 100)) } else { G.sells++; G.received += o.amount * o.price; G.sold += o.amount place("buy", o.price * (1 - STEP_PCT / 100)) } } function main() { if (exchange.GetName() !== "Jupiter") throw new Error("Please add a Jupiter exchange object, current: " + exchange.GetName()) var mode = exchange.IO("trigger.api") var minUsd = mode === "v1" ? 5 : 10 // auto mode may use V2, so assume $10 Log("Limit order API:", mode, ", minimum per order", minUsd, "USD") if (ORDER_USD < minUsd * 1.05) throw new Error("ORDER_USD must not be lower than " + (minUsd * 1.05) + " (minimum " + minUsd + " USD, with a 5% buffer)") G = _G(KEY) if (!G || G.symbol !== SYMBOL) G = { symbol: SYMBOL, center: 0, seq: 0, orders: {}, buys: 0, sells: 0, spent: 0, received: 0, bought: 0, sold: 0 } else Log("Restored from _G:", Object.keys(G.orders).length, "open orders, center price", G.center) while (true) { try { var t = exchange.GetTicker(SYMBOL) if (!t) throw new Error("GetTicker failed: " + GetLastError()) var mid = (t.Buy + t.Sell) / 2 if (!G.center) { buildGrid(mid) } else if (Math.abs(mid / G.center - 1) * 100 > BAND_PCT) { Log("Price", fmt(mid, 4), "left the center", fmt(G.center, 4), "±" + BAND_PCT + "%, canceling all orders and rebuilding the grid", "#FF9900") cancelAll() buildGrid(mid) } else { filledIds(t).forEach(onFill) } _G(KEY, G) var rows = Object.keys(G.orders).map(function (id) { var o = G.orders[id] return [id.length > 24 ? id.slice(0, 12) + "…" + id.slice(-8) : id, o.side === "buy" ? "Buy" : "Sell", o.price, o.amount, fmt((o.price / mid - 1) * 100, 2) + "%"] }).sort(function (a, b) { return b[2] - a[2] }) var pnl = G.received - G.spent + (G.bought - G.sold) * mid LogStatus(_D(), SYMBOL, "Price", fmt(mid, 4), "Center", fmt(G.center, 4), "Rebuild threshold ±" + BAND_PCT + "%", "\nFills Buy", G.buys, "Sell", G.sells, ", estimated PnL of filled orders at current price", fmt(pnl, 4), DRY_RUN ? " (DRY_RUN paper orders)" : "", "\n`" + JSON.stringify({ type: "table", title: "Open Orders", cols: ["Order", "Side", "Price", "Amount", "Distance"], rows: rows }) + "`") } catch (e) { Log("Error in this round:", e.message || e, "#FF0000") } Sleep(INTERVAL * 1000) } }

See Also