JupiterJupiter Developer Platform
Docs
Pricing
Blog
Changelog
Sign In

Blog/Swap

Limit Order v2: From Barter to Money

Limit Order v2: From Barter to Money
yusufyusuf·July 1, 2026·11 min read

Engineering Limit Order v2

Part 1: The Any-Any Problem

Part 2: Correctness Under Failure

Part 3: From Barter to Money

Parts 1 and 2 covered price monitoring at scale and crash-safe execution. Both were hard problems. But the part I keep returning to sits underneath them - a quiet shift in how we think about what an order actually is:

  • In LOv1 - a barter contract: give 100 SOL, receive at least 18,000 USDC.
  • In LOv2 - a price statement: sell SOL at $180.

That looks like a UX improvement. It's an architectural one - that shift is the subject of this final post, and it is the one that made the other two worth building.

The barter contract

Limit Order v1 (LOv1) treated every order, at its core, as a barter contract. You specified 2 quantities - the amount of Token A you were willing to give, and the exact amount of Token B you wanted in return - and the system checked whether the market could satisfy that ratio. If the aggregator could route enough output for your input, the order triggered and executed.

This worked. It was precise, verifiable, and mapped cleanly to how on-chain swaps actually function. You put tokens in, you get tokens out, and the ratio between them varies. The abstraction was honest about what was happening at the protocol level.

But it was honest in the wrong language - precise about ratios, when the user was thinking in prices.

When a user creates a limit order, they are not thinking in exchange ratios. They are thinking in prices. "I want to sell SOL at $180." "I want to buy BONK if it drops to $0.00001." The unit of thought is dollars, not token-per-token ratios. LOv1 forced users to translate their intent - a price they had in mind - into a pair of token amounts, and then executed faithfully against those amounts without ever knowing the price that motivated them. The abstraction leaked from the very first interaction, as users had to do the maths themselves and the system had no way to help.

What ratios can't say

The ratio model had a deeper limitation than bad ergonomics. It couldn't express certain kinds of intent at all.

Under this model, an order says: "give me at least this much output for my input." That sentence has a natural direction. The minimum is a floor, and the order triggers when the market crosses above it, once conditions have improved enough to meet the user's threshold. This maps perfectly to Take Profit: you want to sell SOL, so you mentally convert your target price into an exact output amount and submit that, and the system fires when the market is generous enough to clear it.

But Stop Loss is the inverse. A Stop Loss says: "execute because the price has fallen." The user wants the system to act precisely when conditions have deteriorated, yet the model has no language for that. A minimum-output floor can only say "trigger once the market clears my threshold" - it points in a single direction, towards improving conditions. There is no second field, and no second direction, to say "act when the price drops."

You could imagine overloading the one field you have - reading the minimum as a ceiling instead of a floor, triggering when output falls below it rather than rising above it. But then the same field means opposite things for different orders, and nothing in the model tells the system which reading the user intended. Stop Loss, then, was never something the LOv1 order model could express, and it was never something the product offered.

"150 USDC per SOL" doesn't tell you whether the user is watching the price go up or watching it go down, nor whether crossing that threshold is a good thing (Take Profit) or a bad thing (Stop Loss). The same number means different things depending on which way the user is facing, and the model had no room for that distinction. A ratio doesn't carry direction. The order needed a unit that does.

The price statement

There's an old idea in monetary economics: money is not the thing you transact in, but the thing you think in. You might swap SOL for JUP, but you decided to do it because SOL was at $180. The unit of account - the thing that makes prices comparable, that lets you reason about whether something is expensive or cheap, that gives direction to the very idea of "up" and "down" - is the dollar.

In Part 1, we quoted @jarxiao: money collapses quadratic complexity into linear complexity. There, the collapse was about pricing - one USD price per token instead of a rate for every pair. The same move reshapes the order itself: once a token's price is a single number rather than a relationship to another token, an order can be a statement about that number.

Limit Order v2 (LOv2) expresses orders in USD price instead. "Sell SOL when it hits $180." "Buy BONK when it drops to $0.00001." The trigger condition is a price on a number line, not a ratio between 2 quantities.

This is only possible once you have reliable, real-time USD prices for any token - the in-house price stream that Part 1 leans on to make monitoring scale. Without that foundation, USD-denominated orders are just a nice idea; with it, the barter matrix collapses, and every token's price lives on a single number line, measured in the same unit. And on a number line, direction is unambiguous - above and below mean exactly what they should be, as the condition itself already encodes direction.

Four order types, two primitives

Once orders are denominated in USD, the semantics of every order type fall out of just 2 questions. First: is the user selling or buying? Second: should the order trigger when the price moves above or below a threshold?

That gives you a clean 2×2 matrix:

Trigger when price rises aboveTrigger when price falls below
SellingTake Profit - sell into strengthStop Loss - cut losses
BuyingBuy Above - chase the breakoutBuy Below - buy the dip

Four order types. One state machine. One execution path.

The differences between these types are not in the trigger mechanism - a price crossing a threshold is the same operation regardless of direction. They show up at validation time, when the system fetches a swap quote and checks whether it's acceptable, because "acceptable" means something different for each type:

  • For Take Profit, the quoted output should be at least as good as what the trigger price implies, as the user is expecting favourable conditions.
  • For Stop Loss, the quoted output should be no better than the trigger price implies - if the market has recovered, the stop condition is no longer met and the order should wait.
  • Buy Below validates like Take Profit, and Buy Above like Stop Loss.

These validation differences are just comparisons with different inequality directions. The data model is the same. The trigger logic is the same. The state machine is the same - all 4 are price orders, so all 4 run through the same 19-state machine Part 2 describes, not 4 parallel ones. The entire order lifecycle - creation, deposit, triggering, execution, and withdrawal - runs through a single path, and what used to demand special-cased logic for each order type comes down to which way the comparison points at the moment of execution.

The guarantee

There's a tension at the heart of this design that's worth being honest about.

Triggering at a USD price doesn't mean executing at whatever the market happens to offer. The system stores the user's trigger price, not an explicit expected output amount; at execution time, it lazily derives one from 3 inputs:

  1. The trigger price the user set
  2. The current USD prices of the input and output tokens
  3. The user's slippage tolerance
min output⏟derived floor=f(Ptrigger⏟your price, Pricein÷Priceout⏟live USD rate, s⏟slippage)\underbrace{\text{min output}}_{\text{derived floor}} = f\left( \underbrace{P_{\text{trigger}}}_{\text{your price}},\ \underbrace{\text{Price}_{\text{in}} \div \text{Price}_{\text{out}}}_{\text{live USD rate}},\ \underbrace{s}_{\text{slippage}} \right)derived floormin output​​=f​your pricePtrigger​​​, live USD ratePricein​÷Priceout​​​, slippages​​​

The actual swap quote is then validated against that derived floor. If the market has moved unfavourably between the moment the order triggered and the moment the swap executes - if the price has slipped past what the floor allows - the swap is rejected, the order goes back to waiting, and it retries later. This is the same time-of-check-to-time-of-use (TOCTOU) gap Part 1 flagged in LOv1, except the floor is now derived from live prices and enforced at execution, rather than approximated and discovered too late.

This is a deliberate compromise, and we want to be clear about both sides of it. While LOv1 gave users exact amount accuracy (you specified the ratio, and the system enforced it down to the token), LOv2 trades that exactness for a better mental model. That older guarantee was byte-perfect, because the user's intent was the amounts; the new one asks users to think in prices, and lets the system finally speak the same language. The derived floor means we haven't abandoned protection - your order won't execute at a wildly worse price than you specified - but it isn't the same guarantee LOv1 offered. A layer of derivation now sits between the user's stated price and the actual execution constraint, and that derivation depends on real-time price data that can itself vary.

We think the trade-off is worth it. The default behaviour is protective: your order won't silently execute at a bad price, and if the market moves against you between trigger and execution, the system rejects the swap and waits for better conditions.

On the flipside, the system is also flexible. By adjusting slippage tolerance, users choose how much price movement they're willing to accept: a tighter tolerance means stricter protection, while a wider one means the order is more likely to fill on the first attempt, even if the price has moved. At the maximum, this effectively becomes a "stop market" order - execute as soon as the trigger fires, regardless of where the market is - but that's a deliberate choice the user makes, not the default.

The full picture

Now look at what it took to get here.

Price monitoring rebuilt around a USD price stream (Part 1) made it possible to express orders in USD for any token pair on Solana. A database-backed state machine (Part 2) made the execution of those orders crash-safe and recoverable. And the shift from barter to money (this post) is the conceptual change that made both of those investments worthwhile, because without price-denominated orders you don't need a price stream, and without 4 order types sharing one code path the state machine is solving a far simpler problem.

None of these decisions were individually dramatic. The price stream is a well-understood event-driven pattern, applied carefully; the state machine is textbook durability engineering, applied to Solana's specific failure modes; the order model is a representational shift that a first-year economics student could sketch on a napkin. What we find elegant is how they fit together: each decision created the preconditions for the next, and only in hindsight did we realise the resulting system is simpler than any of the alternatives we weighed along the way.

In retrospect, money is not the thing you transact in - it's the thing you think in. We took that literally, and it turned out to be an architectural principle as much as a product one: when your system thinks in the same units as your users, the abstractions get cleaner, the code paths converge, and features that used to demand special cases fall out for free. There's price connectivity still to extend and edge cases we haven't met yet, but the foundation now speaks the language our users always did.


That wraps the series. If you stuck with all three - thank you. If anything here sparked a question or you've tackled a similar problem differently, reach out to me on X. Now go set a price in dollars, and let the system do what you meant.


yusuf

July 1, 2026