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 order | DCA | |
|---|---|---|
slices | 1 | 2–1000 |
interval | 0 | Seconds between slices (> 0) |
| Fills when | The market reaches your floor | Each interval, if the market is above your floor |
minOut | Your limit price × amount | Your "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"| Parameter | Required | Meaning |
|---|---|---|
in, out, amount, owner | yes | As in /quote; owner signs and gets the output |
minOut | yes, > 0 | Minimum output for the whole amount. Each slice must get at least its pro-rata share: minOutPerSlice = minOut × perSlice / amount |
slices | no (1) | 1–1000, and ≤ amount |
interval | if slices > 1 | Seconds between slices |
expiresIn | no (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:
minOutis 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):
- It reads the open orders (indexed from
OrderCreated/OrderFilled/OrderCancelled) and the chain time from the latest block. - For each due slice it finds the best route with the same planner as
/quote. - If the quote is below the slice's floor, it skips: the limit isn't reached yet.
- Otherwise it calls
fill(id, wrappedNative, splits, fillerMinOut)withfillerMinOut = 99.5%of its own quote, after simulating it. - 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.swapwithrecipient = owner, so the output goes straight to the owner, - moves
nextFillAtforward byinterval; the last slice takes the remainder, so no dust is left.
What the owner is guaranteed
| Guarantee | Enforced by |
|---|---|
| Never less than the floor per slice | OrderBook.fill → Executor minOut check on what the owner actually receives |
| Output goes to the owner, not the keeper | recipient = owner is hard-coded in fill |
Slices no faster than interval | nextFillAt |
Nothing fills after expiry | fill checks it |
| Funds can always come back | cancel by the owner any time; by anyone after expiry; refund always to the owner |
| Fill at market, not at the floor | Our 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| Who | When | Refund to |
|---|---|---|
| Owner | Any time | Owner (cancel) or an address of their choice (cancelTo(id, to), e.g. if the owner can't receive HYPE) |
| Anyone | After expiry | Always the owner |
Order status and notifications
GET /v1/orders adds the keeper's last check to each order, as status.state:
state | Meaning in the UI |
|---|---|
scheduled | Next DCA buy isn't due yet |
waiting_price | Due, but the market is below the floor: "waiting for price" (status.bestOut vs status.minOut) |
no_route | Due, but no route exists right now |
filling | Being filled |
Notifications (GET /v1/notifications?owner=&since=, or live with WS /v1/ws/account and {"owner": "0x…"}):
type | When |
|---|---|
order_created | The order is on-chain |
order_filled / order_completed | A slice filled / the last one did |
order_cancelled | Cancelled (refund in data.refunded) |
order_expired | Expired; the keeper refunded the owner |
dca_skipped | A 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