Documentation

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.

  1. 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.
  2. 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.

Note
Your funds are escrowed by the Diamond (the protocol contract), never by the keeper. The keeper only submits prices — it has no ability to move, hold, or redirect collateral. Execution is non-custodial end to end.

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 forKeeper doesOn-chain call
Open intent — marketFills at the next verified priceexecuteOpen
Open intent — limit / stopFills only once the mark crosses the triggerexecuteOpen
Close request — full or partialSettles the close at the next priceexecuteClose
Take-profit / stop-loss crossedCloses the position at marketexecuteCloseTrigger
Position underwater (equity <= 10% maint.)Liquidates it for the 5% feeliquidatePosition

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.

GuardEnforcesReverts with
Verified priceReport is signed, for this market and chain, <= 60s old, and not future-datedStalePrice / MarketMismatch / FutureTimestamp
SlippageEntry is within your maxSlippageBps (1-500) of the oracle midSlippageExceeded
Liquidatable-at-openThe new position is not already under maintenance marginLiquidatableAtOpen
TriggerA limit/stop fills only once the mark actually crossed itNotTriggered
Vault solvencyThe vault can reserve margin x 9 to fund the max-profit capInsufficientLiquidity
Liquidation gateFires only once equity <= 10% maintenance marginNotLiquidatable

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 taken

Max 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.

Tip
A revert is not a loss. When executeOpen reverts on slippage or a not-yet-crossed trigger, the intent stays pending and the keeper simply retries on the next tick — your fill lands once the spread narrows or the mark crosses. If you would rather walk away, cancelIntent refunds the full escrow.

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 price

Serving 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.

Warning
The keeper is a liveness dependency, not a trust dependency. Thanks to the on-chain guards it can never fill you at an unfair price, close a position you did not flag, or move your escrowed funds — every execute path re-verifies the signed price and reverts if a guard fails. But if the keeper is offline, delayed, or its price feed is stale, your orders can fill late or not fill at all. And on testnet today the oracle is 1-of-1: this single keeper is the sole price signer. m-of-n signing on independent machines is on the roadmap; until then, treat keeper uptime as a real risk.
Note
Testnet honesty: Levant is unaudited, testnet USDG has no monetary value, the oracle is 1-of-1, and ownership is still on the deployer key. None of this is investment advice, and leverage can lose your entire margin.