Skip to content

GuidesLimit orders and DCA

Orders and live data

Limit orders and DCA

Both are the same object in the OrderBook contract: an escrowed amount sold in slices, each slice no earlier than nextFillAt and never below a price floor.

Limit orderDCA
slices12–1000
interval0Seconds between slices (> 0)
Fills whenThe market reaches your floorEach interval, if the market is above your floor
minOutYour limit price × amountYour "never buy below" floor for the whole amount

The order

struct Order {
  address owner;
  address tokenIn;        // address(0) = native HYPE
  address tokenOut;
  uint256 remaining;      // input still escrowed
  uint256 perSlice;       // total / slices
  uint256 minOutPerSlice; // floor for one full slice (> 0)
  uint64  interval;
  uint64  nextFillAt;     // first slice is due at creation
  uint64  expiry;
}

Placing an order

# Limit: sell 1 HYPE for at least 26 USDC, valid 7 days
curl "$API/orders/create?in=HYPE&out=$USDC&amount=1000000000000000000&owner=$ME&minOut=26000000"

# DCA: 100 USDC into HYPE in 4 buys, one per day, skip any buy priced worse than 0.0038 HYPE/USDC
curl "$API/orders/create?in=$USDC&out=HYPE&amount=100000000&owner=$ME&minOut=380000000000000000&slices=4&interval=86400&expiresIn=432000"
ParameterRequiredMeaning
in, out, amount, owneryesAs in /quote; owner signs and gets the output
minOutyes, > 0Minimum output for the whole amount. Each slice must get at least its pro-rata share: minOutPerSlice = minOut × perSlice / amount
slicesno (1)1–1000, and ≤ amount
intervalif slices > 1Seconds between slices
expiresInno (7 days)After expiry, nothing fills and anyone can trigger the refund

The response has order (the computed perSlice, minOutPerSlice, expiry), tx, approval (toward the OrderBook) and simulation, exactly like a quote. Send approval if needed, re-call, then send tx. For native HYPE, tx.value is the full amount.

The input is escrowed at creation by balance difference. Tokens that tax transfers are rejected (InvalidOrder), because the order would be underfunded.

Choosing the floor

  • Limit order: minOut is your price. Example: 1 HYPE at ≥ 26 USDC → minOut=26000000.
  • DCA: the floor is a safety net, not a target. The UI uses "today's quote minus X%" (5/10/20/50%); a slice is skipped and retried later, not lost, when the market is below it.

A zero floor is refused by the API and by the contract: with no floor, a bad route could hand the owner nothing for a slice.

How orders get filled: the keeper

The keeper is a loop in the API process (KEEPER_PRIVATE_KEY set, address allow-listed with OrderBook.setKeeper). Every KEEPER_POLL_MS (5 s):

  1. It reads the open orders (indexed from OrderCreated / OrderFilled / OrderCancelled) and the chain time from the latest block.
  2. For each due slice it finds the best route with the same planner as /quote.
  3. If the quote is below the slice's floor, it skips: the limit isn't reached yet.
  4. Otherwise it calls fill(id, wrappedNative, splits, fillerMinOut) with fillerMinOut = 99.5% of its own quote, after simulating it.
  5. Expired orders with escrow left are cancelled, which refunds the owner.

On-chain, fill:

  • is keeper-only,
  • computes minOut = max(order floor for this slice, fillerMinOut),
  • calls Executor.swap with recipient = owner, so the output goes straight to the owner,
  • moves nextFillAt forward by interval; the last slice takes the remainder, so no dust is left.

What the owner is guaranteed

GuaranteeEnforced by
Never less than the floor per sliceOrderBook.fill → Executor minOut check on what the owner actually receives
Output goes to the owner, not the keeperrecipient = owner is hard-coded in fill
Slices no faster than intervalnextFillAt
Nothing fills after expiryfill checks it
Funds can always come backcancel by the owner any time; by anyone after expiry; refund always to the owner
Fill at market, not at the floorOur keeper's fillerMinOut = 99.5% of its live quote

What is not guaranteed: that a slice fills at all (the market may stay below the floor, or the keeper may be down). The keeper only chooses the route, never the price; and only allow-listed keepers can fill, so a third party can't fill at exactly the floor and keep the surplus.

Listing and cancelling

curl "$API/orders?owner=$ME"            # open orders
curl "$API/orders/cancel?id=12"         # tx to cancel order 12
WhoWhenRefund to
OwnerAny timeOwner (cancel) or an address of their choice (cancelTo(id, to), e.g. if the owner can't receive HYPE)
AnyoneAfter expiryAlways the owner

Order status and notifications

GET /v1/orders adds the keeper's last check to each order, as status.state:

stateMeaning in the UI
scheduledNext DCA buy isn't due yet
waiting_priceDue, but the market is below the floor: "waiting for price" (status.bestOut vs status.minOut)
no_routeDue, but no route exists right now
fillingBeing filled

Notifications (GET /v1/notifications?owner=&since=, or live with WS /v1/ws/account and {"owner": "0x…"}):

typeWhen
order_createdThe order is on-chain
order_filled / order_completedA slice filled / the last one did
order_cancelledCancelled (refund in data.refunded)
order_expiredExpired; the keeper refunded the owner
dca_skippedA DCA buy was due but the price was below the floor: the buy is held until the price comes back (sent once per slice)

Each has an id: keep the last one you showed and poll with since.

Events

event OrderCreated(uint256 indexed id, address indexed owner, address tokenIn, address tokenOut,
                   uint256 total, uint256 perSlice, uint256 minOutPerSlice, uint64 interval, uint64 expiry);
event OrderFilled(uint256 indexed id, address indexed keeper, uint256 amountIn, uint256 amountOut, uint256 remaining);
event OrderCancelled(uint256 indexed id, uint256 refunded);

GET /v1/history?owner= returns them with timestamps.

Fees

Each fill pays the router fee (it goes through the Executor). There is no keeper fee: the operator's keeper pays its own gas.

Next: Live prices

lane.markets · Make the most of every move.