> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nilemarkets.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Keeper Automation

> Permissionless keeper on SettlementEngine: batchSettlePositions() and batchLiquidatePositions() callable by any address; reads getPositionStats() to find work.

The keeper service scans for matured and liquidatable positions and executes batch settlement and liquidation onchain. Because every entry point is permissionless, protocol liveness does not depend on a single operator. This page describes the protocol-level behavior of a keeper — operators building their own keeper can use it as a reference for the onchain interface they must target.

<Note>
  For the publisher service that keeps forward prices fresh onchain, see [Publisher Operations](/deploy/publisher-operations).
</Note>

## Architecture

A keeper is any service that:

1. Reads open position state via `PositionManager.getPositionStats(pairId)`
2. Settles matured positions via `SettlementEngine.batchSettlePositions()`
3. Liquidates undercollateralized positions via `SettlementEngine.batchLiquidatePositions()`
4. Optionally clears matured forward rounds to reclaim gas via `OracleModule.clearMaturedForwards()`

```
                  PositionManager
                          |
                getPositionStats() (read)
                          |
                      [Keeper]
                  (permissionless)
                          |
              batchSettlePositions()
              batchLiquidatePositions()
                          |
                +--------------------+
                | SettlementEngine   |
                +--------------------+
```

The keeper is fully permissionless — any Ethereum address can call the settle and liquidate entry points. No role assignment is required. All other operational choices — cycle interval, retry strategy, telemetry, failover — are implementation decisions left to the operator.

<Warning>
  **What happens if the keeper goes down?** Settlement and liquidation functions are fully permissionless — anyone can call them directly on the `SettlementEngine` contract. If the official keeper is offline, positions can still be settled and liquidated by any Ethereum address. Protocol correctness does not depend on keeper liveness for safety, only for timeliness. The keeper is a convenience, not a requirement.
</Warning>

## Keeper Cycle

The keeper polls for matured and liquidatable positions, then executes batch operations. Settlement and liquidation are independent — one failing does not block the other.

<Steps>
  <Step title="Query Position Stats">
    Call `getPositionStats(pairId)` (read-only) to get counts of matured and liquidatable positions for the EUR/USD pair.
  </Step>

  <Step title="Settle Matured Positions">
    If matured count is greater than 0, call `batchSettlePositions(pairId, maxCount)` to settle matured positions in a single transaction. Matured positions that are also liquidatable are skipped by batch settlement and require a separate liquidation call.
  </Step>

  <Step title="Liquidate Eligible Positions">
    If liquidatable count is greater than 0, call `batchLiquidatePositions(pairId, maxCount)` to liquidate underwater positions in a single transaction.
  </Step>
</Steps>

## Permissionless Entry Points

Anyone can call these functions — no special role is required. This ensures the protocol can always make progress even if the official keeper goes offline.

| Function                                                               | Description                                  | Gas Refund                             |
| ---------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------- |
| `batchSettlePositions(pairId, maxCount)`                               | Settle matured positions in batch            | No                                     |
| `settlePosition(positionId)`                                           | Settle a single matured position             | No                                     |
| `batchLiquidatePositions(pairId, maxCount)`                            | Liquidate eligible positions in batch        | No                                     |
| `liquidatePosition(positionId)`                                        | Liquidate a single eligible position         | No                                     |
| `recordFixingPriceFromPyth(pairId, fixingTimestamp, pythUpdateData[])` | Record fixing price from Pyth                | No (requires `msg.value` for Pyth fee) |
| `clearForwardRound(pairId, fixingTimestamp)`                           | Clear matured forward storage                | Yes (SSTORE deletion refund)           |
| `clearMaturedForwards(pairId)`                                         | Clear all matured forward rounds             | Yes                                    |
| `checkOracleAndPause()`                                                | Check oracle validity, auto-pause if invalid | No                                     |

<Note>
  `recordFixingPriceFromPyth` requires sending ETH as `msg.value` to cover the Pyth oracle update fee. The exact fee can be queried from the Pyth contract's `getUpdateFee()` function.
</Note>

## Gas Cost Responsibility

The current M2 design does not include direct economic incentives for keeper operators. This is intentional for the testnet phase.

* **Settlement and liquidation callers** pay gas but receive no direct reward
* **Liquidation penalty** goes to fee destinations (treasury + pool), not to the liquidator
* **Forward clearing** provides SSTORE refunds — deleting storage slots returns a gas rebate per EIP-3529
* **Oracle fixing** requires the Pyth update fee as `msg.value` in addition to gas costs

## Position-Aware Oracle Cleanup

Forward price data for matured timestamps is no longer needed once all positions at that fixing time are settled. The cleanup functions include safety checks:

* `clearForwardRound(pairId, fixingTimestamp)` checks `positionManager.openPositionCountAtFixingTs(pairId, fixingTimestamp)`. If any open positions still reference that fixing timestamp, the clear is **silently skipped** (not reverted). This prevents clearing forward data that is still needed for open position PnL calculations.

* `clearMaturedForwards(pairId)` resets safeguard baselines (spacing, move limit tracking) after clearing, ensuring fresh publish cycles start clean. This is useful for batch cleanup after all positions at multiple fixing timestamps have been settled.

<Tip>
  Forward clearing is a good candidate for gas-efficient maintenance. The SSTORE deletion refunds can offset a significant portion of the transaction cost, making it economically viable for anyone to call.
</Tip>

<Info>
  In M2, the keeper is operated by the protocol team and there are no direct keeper incentives. M3 plans include economic incentives (keeper rewards, MEV-aware settlement) to attract external keeper operators.
</Info>

## Related Pages

<Columns cols={3}>
  <Card title="Publisher Operations" icon="satellite-dish" href="/deploy/publisher-operations">
    Forward price publication and fixing
  </Card>

  <Card title="Oracle & Forward Pricing" icon="chart-line" href="/protocol/oracle-pricing">
    Product-level pricing overview
  </Card>

  <Card title="Oracle Safeguards" icon="shield-halved" href="/protocol/oracle-safeguards">
    Defense-in-depth oracle protection
  </Card>
</Columns>
