Keeper & Execution
Levant has no order book and no matching engine. Every trade is a two-step flow: you post an intent on-chain and escrow collateral, then an off-chain keeper fills it at the next verified oracle price by submitting a signed price report. The keeper never holds your funds and cannot fill you at an unfair price — every execution path is checked by on-chain guards it has no way around.
Why execution is two-step
A single on-chain transaction cannot both take your order and prove a fresh price. So Levant splits the two. Step one is on-chain and trustless: you submit an intent and your collateral is escrowed by the Diamond. Step two is the keeper: an off-chain service that reads the current oracle price, wraps it in an EIP-712 signed report, and submits it to the contract, which verifies the signature and fills against that exact price. This split is what lets your trade price against a fresh, verified oracle report instead of a stale number already sitting on-chain — and it is why fills are deterministic rather than an auction outcome.
- 1Submit intent and escrow collateral. You call submitOpenIntent (market) or submitLimitIntent (limit/stop) with your market, direction, collateral, leverage, and a mandatory max-slippage. Your collateral moves into the Diamond's escrow. Nothing has filled yet.
- 2Keeper executes with a signed report. The keeper picks up your intent, fetches the price, and calls executeOpen(intentId, signedReport). The contract verifies the report, applies the deterministic spread, checks every guard, and mints the position — or reverts and leaves your intent pending.
Closing is symmetric. You flag the close on-chain — closePosition for a full exit, closePartial for a slice — which escrows nothing new and emits a close request; the keeper then settles it with executeClose. Take-profit and stop-loss are closed by the keeper through executeCloseTrigger, and an underwater position is force-closed through liquidatePosition. Every one of these paths carries a signed price report and settles directly against the Levant Vault in USDG.
What the keeper watches
The keeper is a polling service. On each tick it reads new chain logs (via eth_getLogs, which survives dropped RPC filters), updates an in-memory view of open positions and pending intents, and scans that view against the latest price. On startup it backfills from historical events so a restart never loses track of an open position or an unfilled limit order. It watches three kinds of work:
- Intents. Market intents fill as soon as a verified price is available. Limit and stop intents fill only once the mark crosses the trigger — the keeper decides the timing, but the trigger is re-checked on-chain so it can never fill a limit at a worse-than-trigger price.
- Take-profit / stop-loss. For every position with a TP or SL set, the keeper watches the mark and closes the whole position at market the moment your level is crossed.
- Liquidations. The keeper calls the read-only previewLiquidation for each open position at the current price and, when equity has fallen to the 10% maintenance margin, submits a liquidation.
| Watches for | Keeper does | On-chain call |
|---|---|---|
| Open intent — market | Fills at the next verified price | executeOpen |
| Open intent — limit / stop | Fills only once the mark crosses the trigger | executeOpen |
| Close request — full or partial | Settles the close at the next price | executeClose |
| Take-profit / stop-loss crossed | Closes the position at market | executeCloseTrigger |
| Position underwater (equity <= 10% maint.) | Liquidates it for the 5% fee | liquidatePosition |
It cannot fill you at an unfair price
The keeper chooses when to submit and which price it fetches, but it cannot choose the outcome. executeOpen and executeClose are restricted to the keeper address, yet every fill runs through a chain of on-chain guards, and if any guard fails the whole transaction reverts — no position is minted, no fee is taken, and your escrow is untouched. The report itself is verified in the contract: it must be EIP-712-signed for this market and this chain, no older than 60 seconds, and not dated in the future, or it reverts before any pricing happens.
| Guard | Enforces | Reverts with |
|---|---|---|
| Verified price | Report is signed, for this market and chain, <= 60s old, and not future-dated | StalePrice / MarketMismatch / FutureTimestamp |
| Slippage | Entry is within your maxSlippageBps (1-500) of the oracle mid | SlippageExceeded |
| Liquidatable-at-open | The new position is not already under maintenance margin | LiquidatableAtOpen |
| Trigger | A limit/stop fills only once the mark actually crossed it | NotTriggered |
| Vault solvency | The vault can reserve margin x 9 to fund the max-profit cap | InsufficientLiquidity |
| Liquidation gate | Fires only once equity <= 10% maintenance margin | NotLiquidatable |
Two guards are worth spelling out. The slippage guard compares your spread-adjusted entry to the oracle mid and rejects the fill if it is further away than the tolerance you signed. The liquidatable-at-open guard refuses to mint a position that would already be at or under its maintenance margin at the mark it filled against — at very high leverage the entry spread alone can put you there, and Levant will not open a trade that is dead on arrival (and takes no open fee when it refuses).
// executeOpen — slippage guard
deviationBps = |entryPrice - mid| * 10000 / mid
if deviationBps > maxSlippageBps -> revert SlippageExceeded
// executeOpen — liquidatable-at-open guard
equity = margin + PnL(entryPrice -> mid) // unrealized loss from the entry spread
if equity <= margin * 10% (maintenance) -> revert LiquidatableAtOpen // no open fee takenMax slippage is mandatory and has no implicit default: it must be in the range [1, 500] bps (0.01% to 5%), and the UI offers presets such as 0.10%, 0.30%, 0.50%, and 1.00%. A silent default is exactly how a trader ends up filled far from the price they were shown, so the contract requires you to state one.
Liquidations and TP/SL in detail
Liquidation is permissionless: liquidatePosition is not restricted to the keeper — anyone can submit a signed report and liquidate an underwater position, and whoever does is paid 5% of the position's collateral. In practice the keeper does it, because it is already watching. The liquidation mark is the raw oracle price with no adverse spread applied, the gate only opens once equity has fallen to the 10% maintenance margin (a 90% loss), and only the liquidated position is affected — margin is isolated per position. Take-profit and stop-loss work the same way: the keeper submits when your level is crossed, but executeCloseTrigger re-checks on-chain that the mark really hit your trigger before it settles.
The signed-report endpoint (remove-margin)
Most calls that need a signed price are made by the keeper. One is not: removeMargin, where you withdraw collateral from an open position, is trader-initiated but still needs a signed mark to prove the remaining position stays above maintenance margin. You cannot produce that report yourself — you are not a whitelisted oracle signer. So the keeper runs a small HTTP endpoint that mints a fresh report on request. It binds to loopback and the web app proxies it through a server-side route, so your browser never talks to the keeper directly.
GET /report?market=BTC (keeper, loopback; the web app proxies it via /api/report)
-> 200 { "market": "BTC", "report": "0x...", "price1e18": "..." }
-> 503 { "error": "no price for BTC yet" } // it never serves a stale or absent priceServing a report grants no authority. It is exactly the price the oracle would already accept, so it is public information. It cannot be replayed onto another market (the marketId is inside the signed struct), onto another chain or contract (the EIP-712 domain binds both), and it expires after the 60-second max price age. If the feed has no price yet, the endpoint returns an error rather than a stale one.
It runs as a service
The keeper is off-chain software that runs on a server, not on your machine — that is what keeps the exchange live while you are away. It polls the chain on a fixed interval, serializes its transactions so nonces never collide, and on any failure resets and re-queues the work to retry on the next tick. Its prices come from a multi-source feed (Gate.io first, then KuCoin, then CoinGecko as a fallback) and are signed in chain time, so a chain clock running behind wall-clock never makes a fresh price look future-dated (which the FutureTimestamp check would otherwise reject). The keeper and price signer is the address 0x5040b3387F450Af658f8E7C93ca3F0D44b07E7e4.
