Overview
Keepers are the off-chain operators that hold Atomic's solvency invariant in place. They watch every open position against on-chain prices and call liquidate() on positions that have crossed the 88% margin loss threshold.
The role exists because Ethereum-family chains don't run code on a schedule. Liquidations need an external trigger; keepers are that trigger.
Anyone can run a keeper. The system pays a bounty per liquidation; keepers compete to be first to a closeable position. No whitelist, no staking requirement, no off-chain coordination.
What a keeper does
- Subscribes to position state - usually by indexing
AtomicPositionRegistryevents or polling the subgraph. - Watches on-chain prices for each open position's market via DEX aggregator quotes.
- Detects positions whose current loss is past 88% of margin.
- Submits
liquidate(positionId)toAtomicTrading. - Collects the keeper bounty if the transaction lands first.
The bounty is paid in USDC.e out of the liquidated position's residual margin, before the 12% buffer goes back to the trader.
Why this shape
A few alternatives Atomic doesn't use:
- A single trusted liquidator. One point of failure, censorship risk, and easy to extract MEV around.
- A bonded keeper set. Cuts competition, allows collusion, and needs governance to adjust.
- Auction-based liquidations. Adds latency at exactly the moment latency matters most.
Open competition for a fixed bounty gets the fastest liquidations the protocol can buy, with nobody having to be trusted.
Liquidation incentives
The bounty is sized so that:
- A keeper makes money even on small positions, after gas and opportunity cost.
- The bounty doesn't eat the trader's residual buffer when it doesn't need to.
The exact schedule is part of AtomicTrading configuration and lives in the contract. Roughly: a flat USDC.e component plus a small percentage of position size, scaling with size.
Liquidation fees, including the keeper bounty, are deducted from the residual margin before anything goes back to the trader. On a fast move that eats most of the buffer, the trader can receive zero.
Running a keeper
The keeper interface is fully on-chain. No off-chain registration. A minimal keeper looks like:
while running:
positions = getOpenPositions() # via subgraph or registry events
for p in positions:
price = quoteAggregatorPrice(p.market)
if computeLoss(p, price) >= 0.88 * p.margin:
tx = AtomicTrading.liquidate(p.id)
broadcast(tx)Production keepers go further:
- Mempool monitoring to beat other keepers to the same opportunity.
- Private RPC submission (Flashbots-equivalent on Arbitrum) to avoid getting sniped.
- Gas strategy that lands transactions reliably under congestion.
- Sharded position monitoring to scale to thousands of open positions.
The equilibrium is a handful of well-run keepers splitting the bounty pool. The protocol doesn't need to subsidise this - the bounty is the entire incentive.
Failure modes
- No keeper picks up a position in time. Possible during extreme network congestion. The per-position bounty grows linearly with how far past the threshold the position has drifted, which pulls keepers in even at high gas.
- Keepers collude to delay liquidation. The bounty is paid first-past-the-post; any single non-colluding keeper can break the cartel by submitting first.
- A buggy keeper closes positions early. Can't happen. The on-chain
liquidate()reverts if the threshold hasn't been hit.
What this means if you're a trader
You don't talk to keepers directly. What does matter:
- The 88% threshold is enforced on-chain. A keeper trying to trigger early just gets a revert.
- The actual liquidation doesn't happen exactly at 88% - there's a small lag while keepers race to submit. In practice that's usually a fraction of a basis point past the threshold.
- The keeper bounty is part of the close cost, taken out of your buffer. See Liquidations.