Jupiter Trigger & Recurring Orders as a Strategy Engine
A senior engineer's guide to building a jupiter limit order dca bot strategy on-chain, using Trigger and Recurring programs instead of a custom limit engine.
Most teams that want laddered accumulation on Solana start by writing their own limit-order engine: a keeper loop that polls prices, compares them against a table of target levels, and fires a swap when one crosses. Six weeks later they own a price oracle problem, a nonce-management problem, an RPC-rate-limit problem, and a keeper that occasionally double-fills because two ticks arrived in the same slot. Jupiter already solved most of that on-chain with its Trigger and Recurring programs, and you can build a surprisingly complete strategy engine on top of them without running a single price loop yourself.
This piece is about treating those two programs as primitives — laddered buys, staged take-profit, time-sliced entries — and where the abstraction leaks.
Two programs, two jobs
Jupiter exposes two distinct order types, and mixing them up is the first mistake people make.
Trigger orders (formerly Limit Orders) are conditional. You lock input tokens into a program-owned account and specify an output amount you'll accept. When the market crosses your implied price, Jupiter's keepers route the fill through the standard aggregator and settle it. There's no partial-fill-on-a-schedule; a trigger is "execute this one swap when price is reachable."
Recurring orders are the DCA primitive. You deposit a total budget, set an amount per cycle and an interval, and the program slices your budget into N executions spaced by that interval. The keeper adds jitter to the execution time so your buys aren't perfectly predictable to anyone watching the chain.
The important property of both: the funds sit in a PDA the moment you create the order. You are not holding a signed transaction waiting to broadcast. That removes an entire class of keeper-availability failure. If your bot host dies after order creation, the order still fills.
Laddered accumulation without a price loop
Say you want to accumulate SOL between $180 and $140 across five rungs, heavier at the bottom. In a custom engine you'd watch the price and fire. With Jupiter you place five independent Trigger orders up front and let the keepers do the watching.
The mechanic worth internalizing: a Trigger order's "price" is expressed as a ratio of makingAmount to takingAmount, not a float you pass in. You're saying "I will give up X input for at least Y output." So a rung is just a specific pair of amounts.
// Ladder: buy more SOL as price drops. Amounts in base units.
const USDC = 1e6, SOL = 1e9;
const rungs = [
{ spend: 200 * USDC, minSol: (200 / 180) * SOL }, // ~$180
{ spend: 250 * USDC, minSol: (250 / 170) * SOL },
{ spend: 300 * USDC, minSol: (300 / 160) * SOL },
{ spend: 400 * USDC, minSol: (400 / 150) * SOL },
{ spend: 500 * USDC, minSol: (500 / 140) * SOL }, // heaviest at bottom
];
for (const r of rungs) {
await createTriggerOrder({
inputMint: USDC_MINT,
outputMint: SOL_MINT,
makingAmount: Math.floor(r.spend),
takingAmount: Math.floor(r.minSol), // min SOL out => sets the trigger price
});
}
Each createTriggerOrder returns an unsigned transaction from Jupiter's API that you sign and send. Five rungs means five PDAs and five rent deposits, which brings up the first real gotcha.
Rent and account overhead
Every open order is a funded account. On a wide ladder — say fifteen rungs across a big range — you're paying rent for fifteen accounts simultaneously, and each has an associated token account for the escrowed input. It's small per order, but if you're running dozens of ladders across a portfolio it adds up and, more annoyingly, it consumes account slots you have to track and eventually close. Reconciliation is the tax you pay for not running a keeper. Build the accounting for it early — this is exactly the kind of thing worth pressure-testing in a strategy consultation before you're 80 orders deep and unsure which ones filled.
Take-profit is the same trick, mirrored
The nice symmetry: staged exits are just Trigger orders in the other direction. After your ladder fills and you're holding SOL, you place take-profit rungs where inputMint is SOL and takingAmount is the USDC you want back. Scale out 20% at each level and you've got a laddered exit that needs zero monitoring.
Where this beats a hand-rolled engine is the failure mode. A custom TP bot that goes offline during a spike misses the exit. A Jupiter Trigger order fills whether or not your infrastructure is alive. That single property is why I reach for it even on strategies where I'd otherwise want tighter control. The tradeoff, of course, is that you accept the aggregator's routing and fill timing — if you need sub-second, price-improving execution against a specific venue, this isn't it, and you're better off on a purpose-built perps stack like a Hyperliquid perps bot where you own the order lifecycle.
Time-slicing entries with Recurring
Ladders assume you have a price opinion. Sometimes you just want in over time and don't want to guess levels. That's Recurring's job.
await createRecurringOrder({
inputMint: USDC_MINT,
outputMint: SOL_MINT,
inAmount: 3000 * USDC, // total budget
numberOfOrders: 30, // ~$100 per slice
interval: 86400, // one execution per day
});
Thirty daily $100 buys, keeper-executed, jittered so you're not front-runnable on a fixed schedule. You can layer this under a ladder: recurring as a slow base accumulation, triggers as opportunistic dip-buys. The two compose cleanly because neither one needs to know the other exists.
One subtlety on Recurring: it fills at whatever the aggregator quotes at execution, with a slippage bound you set. In thin markets a slice can execute at a meaningfully worse price than your average expectation. For volatile or low-liquidity pairs I keep slice sizes small relative to typical pool depth, which is the same discipline you'd apply when sizing into a DLMM fee-farming position.
Where the abstraction leaks
A few things that bit me or people I've reviewed:
- Fills route through the aggregator, so your effective price includes routing and price impact. The trigger ratio is a minimum acceptable output, not a guaranteed clean fill at that level. Set
takingAmountwith impact in mind. - Keeper timing is not instant. There's a lag between price becoming reachable and the fill landing. On a fast wick, a tight trigger might get skipped if price passes through and back before a keeper acts.
- Order state lives on-chain, so your bot must reconcile against it, not against its own memory. Never treat a local "I placed this" record as truth. Query the program accounts. If you're wiring a live view of open orders and fills, that reconciliation logic is the heart of a decent trading dashboard.
- Fees. Jupiter takes a small cut on filled orders. Model it into your rung spacing, or your take-profit ladder will quietly underperform your spreadsheet.
If you're moving real size through this, the create/sign/reconcile path is exactly where subtle bugs hide — a wrong decimal on takingAmount is a silently mispriced rung. It's worth putting that flow through a code review before it touches mainnet funds.
The broader point: Jupiter's Trigger and Recurring programs are a genuine strategy engine if you think of them as composable order primitives rather than a UI feature. Ladders, staged exits, and time-sliced DCA all fall out of two building blocks, and you inherit keeper-executed, host-independent fills for free. It pairs well with more active strategies — a funding-capture setup like the Hyperliquid spot-perp funding loop for yield, or a just-in-time LP strategy on Meteora for fee income — with Jupiter orders handling the patient accumulation underneath.
If you're deciding whether to build on these primitives or roll your own engine, that's the exact call worth talking through in a strategy consultation before you write the first line.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article