Service Types
| Parameter | Publisher | Keeper |
|---|---|---|
| Access | PUBLISHER_ROLE (access-controlled) | Permissionless |
| Cycle interval | Configurable; default = maxForwardAge / 2 | Configurable; default = chain block time |
| Batch size | All enabled tenors per cycle (single tx) | Up to 50 positions per batch (~10M gas) |
| Retry strategy | 3 attempts, exponential backoff | Automatic provider-level retries |
| Nonce handling | Explicit fetch before each write | Provider-managed nonce tracking |
| Failure behavior | Log error, mark unhealthy, continue | Log warning, mark unhealthy, continue |
Arc<WalletProvider>) to ensure a unified nonce sequence and prevent nonce collisions when sending transactions from the same wallet.
Publisher Cycle
The publisher is responsible for keeping forward prices fresh onchain and recording fixing prices at maturity. It requires thePUBLISHER_ROLE granted by the protocol admin.
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.
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.
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.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.Query Position Stats
Call
getPositionStats(pairId) (read-only) to get counts of matured and liquidatable positions for the EUR/USD pair.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.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 |
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.valuein 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)checkspositionManager.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.
Observability
M1-M2: Health Check Endpoints
M1-M2: Health Check Endpoints
Each service exposes an HTTP health-check endpoint returning JSON status:
- Publisher:
GET /healthon port 8080 — reports healthy flag, last publish timestamp, cumulative publish count - Keeper:
GET /healthon port 8081 — reports healthy flag, last activity timestamp, cumulative settlement and liquidation counters
M3 Planned: Full Observability
M3 Planned: Full Observability
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