Skip to main content
The Open Nile Protocol requires off-chain services to maintain liveness. Two automated services — the Publisher and the Keeper — handle price publication, settlement, and liquidation. All settlement and liquidation functions are permissionless, ensuring protocol liveness does not depend on a single operator.

Service Types

ParameterPublisherKeeper
AccessPUBLISHER_ROLE (access-controlled)Permissionless
Cycle intervalConfigurable; default = maxForwardAge / 2Configurable; default = chain block time
Batch sizeAll enabled tenors per cycle (single tx)Up to 50 positions per batch (~10M gas)
Retry strategy3 attempts, exponential backoffAutomatic provider-level retries
Nonce handlingExplicit fetch before each writeProvider-managed nonce tracking
Failure behaviorLog error, mark unhealthy, continueLog warning, mark unhealthy, continue
Both services share a single wallet provider instance (Arc<WalletProvider>) to ensure a unified nonce sequence and prevent nonce collisions when sending transactions from the same wallet.
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.

Publisher Cycle

The publisher is responsible for keeping forward prices fresh onchain and recording fixing prices at maturity. It requires the PUBLISHER_ROLE granted by the protocol admin.
1

Fetch Spot Price

Fetch the current EUR/USD spot price from the Pyth Hermes API. The publisher retries up to 3 times if the returned timestamp drifts more than 30 seconds from the requested fixing time.
2

Calculate Forward Prices

Calculate forward prices for all enabled tenors (1D, 1W, 1M) using the forward pricing formula. Each tenor produces a price based on the spot rate, interest rate differential, and time to maturity.
3

Publish Forward Rounds

Submit a single batch publishForwardRounds transaction that updates all tenor prices atomically. This ensures all forward prices are from the same spot observation.
4

Record Fixing Prices

For any matured timestamps, submit separate recordFixingPriceFromPyth transactions to lock in the settlement price. These are non-fatal on failure — the publisher logs the error and continues to the next cycle.
Fixing-price recording is critical for settlement but separated from forward publishing to avoid blocking the main cycle. If a fixing price record fails, the keeper will skip settlement for those positions until the fixing price is available.

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

Query Position Stats

Call getPositionStats(pairId) (read-only) to get counts of matured and liquidatable positions for the EUR/USD pair.
2

Settle Matured Positions

If matured count is greater than 0, call batchSettlePositions(pairId, maxCount) to settle up to 50 positions per transaction. Matured positions that are also liquidatable are skipped by batch settlement and require a separate liquidation call.
3

Liquidate Eligible Positions

If liquidatable count is greater than 0, call batchLiquidatePositions(pairId, maxCount) to liquidate up to 50 underwater positions per transaction.

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.
FunctionDescriptionGas Refund
batchSettlePositions(pairId, maxCount)Settle matured positions in batchNo
settlePosition(positionId)Settle a single matured positionNo
batchLiquidatePositions(pairId, maxCount)Liquidate eligible positions in batchNo
liquidatePosition(positionId)Liquidate a single eligible positionNo
recordFixingPriceFromPyth(pairId, fixingTimestamp, pythUpdateData[])Record fixing price from PythNo (requires msg.value for Pyth fee)
clearForwardRound(pairId, fixingTimestamp)Clear matured forward storageYes (SSTORE deletion refund)
clearMaturedForwards(pairId)Clear all matured forward roundsYes
checkOracleAndPause()Check oracle validity, auto-pause if invalidNo
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.

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
In M2, the keeper is operated by the protocol team. M3 will introduce economic incentives (keeper rewards, MEV-aware settlement) to attract external keeper operators.

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

Observability

Each service exposes an HTTP health-check endpoint returning JSON status:
  • Publisher: GET /health on port 8080 — reports healthy flag, last publish timestamp, cumulative publish count
  • Keeper: GET /health on port 8081 — reports healthy flag, last activity timestamp, cumulative settlement and liquidation counters
There is no centralized alerting, metrics pipeline, or on-call integration. Operators must manually poll health endpoints or inspect service logs.
M3 scope includes:
  • Structured metrics export (Prometheus-compatible)
  • Dashboards for publish cycle timing, settlement latency, and liquidation counts
  • Alerting on missed publish cycles or stale settlements
  • Dead-man-switch monitoring for keeper liveness

Architecture Summary

                    Pyth Hermes API
                         |
                    [Publisher]
                    (PUBLISHER_ROLE)
                         |
          publishForwardRounds()
          recordFixingPriceFromPyth()
                         |
                  +--------------+
                  | OracleModule |
                  +--------------+
                         |
         getPositionStats() (read-only)
                         |
                     [Keeper]
                  (permissionless)
                         |
          batchSettlePositions()
          batchLiquidatePositions()
                         |
             +--------------------+
             | SettlementEngine   |
             +--------------------+
The publisher writes price data to the OracleModule. The keeper reads position state and executes settlement and liquidation through the SettlementEngine. Both services operate independently and can be restarted without affecting each other.