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

Buys the same token at regular intervals to average down the position cost: on Jupiter it uses native DCA, and on other DEXs the strategy places market buys on a schedule. When the price is below the moving average, that period's buy amount is doubled.

How It Works

The strategy picks its execution mode based on the exchange object:

  1. Jupiter with NATIVE set to true: uses native DCA through exchange.IO("dca.*", ...). dca.create deposits the full TOTAL up front, and Jupiter's keeper buys one PER_ORDER tranche every INTERVAL_HOURS hours, so the strategy does not need to keep running. MIN_PRICE and MAX_PRICE restrict buys to that price range (when the price is outside the range, the round is skipped or deferred). On startup, the strategy first calls dca.list to check whether the trading pair already has an active DCA order and creates one only if none exists (the returned ID is also saved via _G to prevent duplicate creation). After that, it calls dca.list every minute to display execution progress. Recurring v1 (no API Key configured) requires at least $50 per tranche and at least 2 tranches; Trigger V2 (API Key configured and quote currency USDC or USDT) requires at least $10 per tranche, with funds held in a Jupiter-managed vault.
  2. Other DEXs (Uniswap, PancakeSwap, Aerodrome, Velodrome, etc.), or NATIVE set to false: every INTERVAL_HOURS hours the strategy buys PER_ORDER with a market order (exchange.CreateOrder(交易对, "buy", -1, 金额); for market buy orders the amount is denominated in the quote currency). When the best ask is DIP_PCT% below the MA_DAYS-day moving average, that period's amount is multiplied by DIP_MULT (buy more on dips), and total spending never exceeds TOTAL. Each fill is booked after confirmation via exchange.GetOrder, and progress is saved with _G() so execution can resume after a restart.

Exchange Object

One DEX exchange object (a dedicated wallet is recommended). The wallet must hold the quote currency plus a small amount of the native coin to pay fees. When SYMBOL is null, the trading pair configured on the exchange object is used.

Parameters
ParameterDefaultDescription
SYMBOLnullTrading pair; null means the trading pair configured on the exchange object
TOTAL500Total budget (in quote currency)
PER_ORDER50Amount per tranche (in quote currency)
INTERVAL_HOURS24Interval between tranche buys (hours)
NATIVEtrueUse native DCA on Jupiter (dca.create)
MIN_PRICEnullNative DCA: skip the round when the price is below this value (null means no limit)
MAX_PRICEnullNative DCA: skip the round when the price is above this value (null means no limit)
DIP_PCT5Strategy-driven buys: buy extra when the price is this percentage below the moving average
DIP_MULT2Strategy-driven buys: multiplier for dip buys
MA_DAYS20Strategy-driven buys: moving average period in days
DRY_RUNtrueWhen true, only simulates and spends no real funds

Risks
  • DCA only averages your entry cost; it cannot prevent losses from a falling market.
  • Every on-chain transaction incurs gas or network fees, so small, high-frequency buys are not cost-effective.
  • Market orders are subject to slippage (see exchange.IO("slippage")).

DRY_RUN vs. Live Trading

When DRY_RUN is true (the default), no real funds are spent: native DCA uses dca.simulate to simulate the creation transaction (reason gives the reason it failed, if any). Strategy-driven buys use exchange.IO("route") to get a quote and record a paper fill, and EVM DEXs also use exchange.IO("simulate") to simulate the swap transaction (the simulation errors out when the wallet balance is insufficient, which is expected). Set DRY_RUN to false to go live. Once created, a native DCA order is executed by Jupiter, so stopping the bot does not stop the DCA; to cancel it, use dca.cancel.

Examples

Complete strategy code (DRY_RUN = true: simulation only, no real funds are spent).

javascript
/* * DEX DCA (dollar-cost averaging, example strategy) * * Two implementations, chosen automatically based on the exchange object: * 1. Jupiter (Solana) with NATIVE = true: uses Jupiter's native DCA exchange.IO("dca.create", …) — the full amount is deposited up front, * and Jupiter's keeper buys one tranche every INTERVAL_HOURS hours, so the strategy does not need to keep running; MIN_PRICE / MAX_PRICE can restrict * buys to a price range (a round outside the range is skipped or deferred). The strategy only creates the DCA order and shows its progress via dca.list. * Recurring v1 (without an API Key) requires at least $50 per order and at least 2 orders; Trigger V2 (with an API Key and a quote currency of * USDC / USDT) requires at least $10 per order, with funds held in a vault custodied by Jupiter. * 2. Other DEXs (Uniswap, PancakeSwap, Aerodrome, Velodrome, etc.), or NATIVE = false: the strategy itself buys, every * INTERVAL_HOURS hours, coins worth PER_ORDER of the quote currency with a market order; when the price is DIP_PCT% below the MA_DAYS-day moving average, that order's amount * is multiplied by DIP_MULT (buying the dip). Total spending never exceeds TOTAL. * Progress is saved in _G, so execution can resume after a restart. * * With DRY_RUN = true (default) no funds are spent: native DCA uses IO("dca.simulate", …) to simulate the creation transaction; the self-buying mode uses * IO("route", …) to get quotes and records paper fills, and on EVM DEXs also uses IO("simulate", …) to simulate the swap transaction (simulation errors are normal when the wallet balance is insufficient). * * Risk warning: DCA only averages out the entry cost and cannot prevent losses in a downtrend; every on-chain transaction incurs gas / network fees, so small high-frequency buys are not cost-effective; * market orders are subject to slippage (see exchange.IO("slippage")). * * Usage: add a DEX exchange object (a dedicated wallet is recommended), and fund the wallet with the quote currency plus a small amount of the native coin for fees. */ // ---------------- Parameters ---------------- var SYMBOL = null // Trading pair; null = the pair configured on the exchange object var TOTAL = 500 // Total budget (quote currency) var PER_ORDER = 50 // Amount per order (quote currency) var INTERVAL_HOURS = 24 // Interval between buys (hours) var NATIVE = true // Use native DCA (dca.create) on Jupiter var MIN_PRICE = null // Native DCA: skip the round when the price is below this value (null = no limit) var MAX_PRICE = null // Native DCA: skip the round when the price is above this value (null = no limit) var DIP_PCT = 5 // Self-buying: buy more when the price is this % below the moving average var DIP_MULT = 2 // Self-buying: multiplier for dip buys var MA_DAYS = 20 // Self-buying: moving average period in days var DRY_RUN = true // true = simulation only, no funds are spent function fmt(x, d) { return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d) } // ---------------- Method 1: Jupiter native DCA ---------------- function nativeDca() { var key = "dca_native_" + SYMBOL // ID of the created DCA order, prevents duplicate creation after a restart var params = { symbol: SYMBOL, side: "buy", total: TOTAL, perOrder: PER_ORDER, interval: INTERVAL_HOURS * 3600 } if (MIN_PRICE) params.minPrice = MIN_PRICE if (MAX_PRICE) params.maxPrice = MAX_PRICE var active = exchange.IO("dca.list", { status: "active", symbol: SYMBOL }) if (!active) throw new Error("dca.list failed: " + GetLastError()) if (active.length === 0 && !_G(key)) { if (DRY_RUN) { var sim = exchange.IO("dca.simulate", params) if (!sim) Log("dca.simulate error:", GetLastError()) else Log("dca.simulate (" + sim.version + ")", sim.ok ? "passed" : "failed: " + sim.reason, "plan:", JSON.stringify(sim.plan)) } else { var r = exchange.IO("dca.create", params) if (!r) throw new Error("dca.create failed: " + GetLastError()) Log("DCA created", r.version, r.id, "per order", r.perOrder, "total", r.numberOfOrders, "orders", "#00AA00") _G(key, r.id) } } while (true) { try { var rows = [] ;["active", "history"].forEach(function (st) { var list = exchange.IO("dca.list", { status: st, symbol: SYMBOL }) if (!list) throw new Error("dca.list failed: " + GetLastError()) list.forEach(function (d) { rows.push([d.id, d.version, d.state, fmt(d.perOrder, 4), (d.ordersFilled === null ? "?" : d.ordersFilled) + " / " + d.numberOfOrders, fmt(d.spent, 4), fmt(d.filledQty, 6), fmt(d.avgPrice, 4), d.nextFillAt ? _D(d.nextFillAt) : "-"]) }) }) LogStatus(_D(), SYMBOL, "Jupiter native DCA", DRY_RUN ? "(DRY_RUN: creation was only simulated; the table below shows DCA orders already in the wallet)" : "", "\n`" + JSON.stringify({ type: "table", title: "DCA", cols: ["ID", "Version", "State", "Per Order", "Orders Filled", "Spent", "Bought", "Avg Price", "Next"], rows: rows }) + "`") } catch (e) { Log("Error in this round:", e.message || e, "#FF0000") } Sleep(60000) } } // ---------------- Method 2: the strategy buys on its own schedule ---------------- function buyOnce(D, amount, price) { var q = exchange.IO("route", SYMBOL, "buy", amount / price) // Quote only: how much it costs to buy this quantity if (DRY_RUN) { if (exchange.GetName() !== "Jupiter") { var sim = exchange.IO("simulate", SYMBOL, "buy", amount) // Market buy: amount is denominated in the quote currency Log(sim ? "simulate passed: " + sim.route : "simulate failed (normal when the wallet balance is insufficient): " + GetLastError()) } if (!q || !q.price) { Log("Quote failed:", GetLastError()); return false } D.spent += amount; D.bought += amount / q.price Log("DRY_RUN buy", fmt(amount, 2), "→", fmt(amount / q.price, 6), "@", fmt(q.price, 4)) return true } var id = exchange.CreateOrder(SYMBOL, "buy", -1, amount) if (!id) { Log("Buy failed:", GetLastError(), "#FF0000"); return false } for (var i = 0; i < 60; i++) { var o = exchange.GetOrder(id) if (o && o.Status === ORDER_STATE_CLOSED) { D.spent += amount; D.bought += o.DealAmount Log("Bought", fmt(amount, 2), "→", o.DealAmount, "@", fmt(o.AvgPrice, 4), "#00AA00") return true } if (o && o.Status === ORDER_STATE_CANCELED) { Log("Buy order not filled (canceled / failed on-chain)", id, "#FF0000"); return false } Sleep(2000) } Log("Buy order not confirmed within two minutes; recorded as spent, please verify later", id, "#FF0000") D.spent += amount return true } function loopDca() { var key = DRY_RUN ? "dca_loop_paper" : "dca_loop" var D = _G(key) if (!D || D.symbol !== SYMBOL) D = { symbol: SYMBOL, spent: 0, bought: 0, buys: 0, lastBuy: 0 } while (true) { try { var t = exchange.GetTicker(SYMBOL) if (!t) throw new Error("GetTicker failed: " + GetLastError()) var r = exchange.GetRecords(SYMBOL, PERIOD_D1) var ma = r && r.length >= MA_DAYS ? TA.MA(r, MA_DAYS)[r.length - 1] : null var dip = ma !== null && t.Sell < ma * (1 - DIP_PCT / 100) var due = Date.now() - D.lastBuy >= INTERVAL_HOURS * 3600000 if (due && D.spent < TOTAL - 1e-9) { var amount = Math.min(PER_ORDER * (dip ? DIP_MULT : 1), TOTAL - D.spent) Log("DCA order #", D.buys + 1, "amount", fmt(amount, 2), dip ? "(below MA by " + DIP_PCT + "%, ×" + DIP_MULT + ")" : "") if (buyOnce(D, amount, t.Sell)) { D.buys++; D.lastBuy = Date.now() } _G(key, D) } var avg = D.bought > 0 ? D.spent / D.bought : null var value = D.bought * t.Buy var table = { type: "table", title: SYMBOL + " DCA" + (DRY_RUN ? " (DRY_RUN paper)" : ""), cols: ["Item", "Value"], rows: [ ["Orders Filled", D.buys], ["Spent / Total Budget", fmt(D.spent, 2) + " / " + TOTAL], ["Bought", fmt(D.bought, 6)], ["Avg Entry Price", fmt(avg, 4)], ["Current Price (Best Bid)", fmt(t.Buy, 4)], [MA_DAYS + "-day MA", fmt(ma, 4) + (dip ? " (dip)" : "")], ["Market Value / PnL", fmt(value, 2) + " / " + fmt(value - D.spent, 2)], ["Next Buy", D.spent >= TOTAL - 1e-9 ? "Budget exhausted" : _D(D.lastBuy + INTERVAL_HOURS * 3600000)]] } LogStatus(_D(), "\n`" + JSON.stringify(table) + "`") } catch (e) { Log("Error in this round:", e.message || e, "#FF0000") } Sleep(60000) } } function main() { SYMBOL = SYMBOL || exchange.GetCurrency() if (exchange.GetName() === "Jupiter" && NATIVE) nativeDca() else loopDca() }

See Also