A Chainlink CRE-powered, auction-based Oracle Extractable Value capture system for DeFi lending protocols.
This system replaces chaotic MEV gas wars with a deterministic, two-phase auction that fairly allocates liquidation rights, ensures protocol solvency, and distributes captured OEV between competitive liquidators and the protocol treasury. The architecture spans on-chain smart contracts, a multi-service offchain coordination layer, a Python agent fallback network, and a Rust terminal interface — all wired together through a commit-reveal oracle feed and a public bid endpoint.
- Auction winner execution (
testnet-sim-liquidatorpath): https://sepolia.basescan.org/tx/0x1db3b8faeacdf0ae6a731b5c6c2e21fc1f41cc3cd496cb241b4bab7d284f5afe - Fallback execution via ERC-8004 registered fallback agent: https://sepolia.basescan.org/tx/0x41f607daef6d3624952659cd732843f03c88576ed1dde7bba2d828ba3255c594
Network: base-sepolia
Chain ID: 84532
| Contract | Address |
|---|---|
SourcePriceOracle |
0x3E092C1D36cC779ABAcD40Df0e52b5d4fFbA4Abd |
SVRFeed |
0x950eb14c49d2fa4fb7d146f2886704b8e3f02b82 |
LendingMarket |
0xb40e2240335e2423a33b0e4067f6144266ab1d7f |
LiquidationExecutor |
0xbb07962898a13587af84cbbfcb25c6808bac19ca |
SwapRouter |
0x5b8dc9759c8c128951cc7cdbf9ee469c48607858 |
Treasury |
0x04dce57655a0ced6f5f356be684fbb8daa02e973 |
AgentIdentityRegistry |
0x8004A818BFB912233c491871b3d84c89A494BD9e |
CollateralToken |
0x7f7a2f064309a783e8f4e7894bd3a53fd86d1cb0 |
DebtToken |
0xd6162b3bac2873eb8fb5b40cc513f3d907aba4db |
| EOA Role | Address |
|---|---|
deployer |
0x8a94fdF8D895B0DA01c8649559e2E5e7c03d2CCf |
updater |
0x8a94fdF8D895B0DA01c8649559e2E5e7c03d2CCf |
- OEV Liquidation Pipeline
- Table of Contents
- Demo Proof (Base Sepolia)
- 1. What is OEV and Why It Matters
- 2. The Core Design: Two-Phase Price Architecture
- 3. System Architecture
- 4. End-to-End Liquidation Flow
- 5. Service Layer — Component Reference
- 6. Smart Contract Layer
- 7. Resilience, Persistence, and Recovery
- 8. Execution Modes
- 9. TUI Monitor
- 10. Getting Started
- 11. Configuration Reference
- 12. API Reference
- 13. Contributing
- 14. License
Oracle Extractable Value is a subset of MEV that arises specifically from oracle price updates. In a lending protocol, the moment an oracle publishes a price that makes a borrower's health factor drop below 1.0, a liquidation becomes profitable. In the absence of any coordination layer, every bot on the network sees this opportunity simultaneously and competes for it through priority gas auctions — a zero-sum game where most of the extracted value flows to block producers rather than back to the protocol.
This system captures that value at the source. By wrapping the oracle update itself inside a commit-reveal round, and auctioning off the exclusive right to execute the liquidation before the price is revealed, it converts an unstructured race into a sealed-bid auction. The winning bid represents the liquidator's willingness to pay for exclusivity. That payment flows to the protocol treasury. The liquidator still profits from the liquidation bonus. The borrower's position is resolved before it can accumulate bad debt. Every participant in the protocol benefits compared to the MEV alternative.
The auction is public. Any participant that can construct a valid signed liquidation transaction and submit it to the /bid endpoint before the bid window closes is eligible to win. The liquidators in this repository participate automatically via server-sent events, but they hold no structural advantage over external bidders who satisfy the same validation rules.
The single most important design decision in this system is the deliberate separation between the price used for candidate detection and the price used for liquidation execution. Understanding this separation is necessary to understand every other component.
Timeline ──────────────────────────────────────────────────────────────────►
Block N Block N+K (Reveal Block)
│ │
│ SourcePriceOracle.updatePrice() │ SVRFeed.reveal(price, salt)
│ emits SourcePriceUpdated │ LendingMarket price updated
│ │
▼ ▼
╔══════════════════════════════╗ ╔══════════════════════════════════════╗
║ PHASE 1: DETECTION ║ ║ PHASE 2: EXECUTION ║
║ ─────────────────────────── ║ ║ ────────────────────────────────── ║
║ Price: Source trigger price ║ ║ Price: SVRFeed revealed price ║
║ ║ ── ║ ║
║ CRE reads source event ║ ║ LendingMarket.liquidate() runs ║
║ Indexer screens candidates ║ ║ Health factor checked at THIS price ║
║ Coordinator opens round ║ ║ Winner tx broadcast ║
║ priceHash committed onchain ║ ║ Profit settled atomically ║
║ Auction bid window open ║ ║ ║
╚══════════════════════════════╝ ╚══════════════════════════════════════╝
│ │
│ Exclusivity window active │
│ Only LiquidationExecutor │
│ can liquidate │
└────────────────────────────────────┘
Phase 1 uses the source oracle event as an early signal. The lending market price has not changed yet — SVRFeed still holds the previous value. CRE and the indexer use the source price purely for candidate screening: would this borrower become liquidatable if the feed updates to this price? This pre-selection determines whether a round is worth opening.
Phase 2 is where economic reality settles. At the reveal block, SVRFeed.reveal() pushes the new price into the lending market. All liquidation health-factor checks run against this revealed price. A liquidator who won the auction may find that the revealed price produced slightly different economics than the source price suggested — this is expected and is factored into rational bid computation.
This design means the system never speculatively executes liquidations. It only acts when an oracle update makes positions genuinely unhealthy, and the auction ensures that when it does act, the right participant executes at the right price.
The system is composed of five layers: on-chain contracts, the offchain coordination stack, the CRE trigger service, the agent fallback network, and the Rust TUI. The diagram below shows how these layers interconnect at runtime.
graph TD
subgraph Blockchain["On-Chain"]
SPO["SourcePriceOracle\n─────────────────\nupdatePrice()\nemits SourcePriceUpdated"]
SVR["SVRFeed\n─────────────────\ncommit(priceHash)\nreveal(price, salt)\nlatestRoundData()"]
LM["LendingMarket\n─────────────────\nonPriceCommit()\nliquidate()\nhealth factor checks"]
LE["LiquidationExecutor\n─────────────────\npull debt\ncall market liquidate\nswap collateral"]
SR["SwapRouter\n─────────────────\nroute collateral swap\nreturn proceeds"]
AIR["AgentIdentityRegistry\n─────────────────\nERC-8004\nRegistered / URIUpdated"]
end
subgraph Offchain["Offchain Coordination Stack"]
CRE["CRE\n─────────────────\nonSourcePriceLog()\nstale-position cron\nguardian emergency mode"]
COORD["Coordinator\n─────────────────\nround state machine\nauction controller\nbid validator\nbackstop activator"]
IDX["Indexer\n─────────────────\nevent sourcing\ncandidate math\nposition tracking\nstate persistence"]
LIQ1["Liquidator A\n─────────────────\nSSE consumer\nprofit solver\nbid submitter"]
LIQ2["Liquidator B"]
LIQ3["Liquidator N"]
RW["Registry Watcher\n─────────────────\nERC-8004 events\nagentDirectory.json\nhealth prober"]
BS["Backstop\n─────────────────\nfilter active agents\nrank by priority\nstandby + activate"]
end
subgraph Agents["Agent Fallback Network"]
AG1["Liquidation Agent\n─────────────────\n/standby /activate\nliquidate_borrower tool"]
AG2["Liquidation Agent"]
AGN["Liquidation Agent N"]
end
subgraph TUI["TUI Monitor (Rust)"]
DASH["Dashboard\nservice health\nlatest round"]
ROUNDS["Rounds\ncoordinator state"]
POS["Positions\nborrower states"]
end
SPO -->|"SourcePriceUpdated event"| CRE
CRE -->|"/candidates?price1e8=..."| IDX
CRE -->|"/cre/start-round"| COORD
COORD -->|"SVRFeed.commit(priceHash)"| SVR
COORD -->|"LendingMarket.onPriceCommit()"| LM
SVR -->|"latestRoundData()"| LM
COORD -->|"SSE /auction/stream"| LIQ1
COORD -->|"SSE /auction/stream"| LIQ2
COORD -->|"SSE /auction/stream"| LIQ3
LIQ1 -->|"/bid"| COORD
LIQ2 -->|"/bid"| COORD
LIQ3 -->|"/bid"| COORD
COORD -->|"SVRFeed.reveal()"| SVR
COORD -->|"broadcast winner tx"| LE
LE -->|"LendingMarket.liquidate()"| LM
LE -->|"swap collateral"| SR
COORD -->|"activate backstop"| BS
AIR -->|"Registered / URIUpdated"| RW
RW -->|"agentDirectory.json"| BS
BS -->|"/standby + /activate"| AG1
BS -->|"/standby + /activate"| AG2
BS -->|"/standby + /activate"| AGN
AG1 -->|"LendingMarket.liquidate()"| LM
COORD -->|"rounds API"| ROUNDS
IDX -->|"/positions"| POS
COORD -->|"health API"| DASH
style Blockchain fill:#0d1117,stroke:#30363d,color:#e6edf3
style Offchain fill:#0d1117,stroke:#30363d,color:#e6edf3
style Agents fill:#0d1117,stroke:#30363d,color:#e6edf3
style TUI fill:#0d1117,stroke:#30363d,color:#e6edf3
Service composition and default ports are defined in offchain_stack.sh. Each service is independently deployable and communicates exclusively over HTTP — no shared in-process state between the coordinator, indexer, liquidators, and registry watcher.
The following sequence diagram traces a complete liquidation from oracle price event to profit settlement, covering the nominal path (auction winner succeeds) and indicating where the fallback branch begins.
sequenceDiagram
autonumber
participant Oracle as SourcePriceOracle
participant CRE as CRE
participant IDX as Indexer
participant COORD as Coordinator
participant SVR as SVRFeed
participant LM as LendingMarket
participant LIQA as Liquidator A
participant LIQB as Liquidator B
participant LE as LiquidationExecutor
participant SR as SwapRouter
participant BS as Backstop
Oracle->>CRE: SourcePriceUpdated(price1e8)
CRE->>IDX: GET /candidates?price1e8=...
IDX-->>CRE: [candidate borrowers] or []
Note over CRE: Empty candidates → stop, no round
CRE->>COORD: POST /cre/start-round {price1e8, sourceRoundKey}
Note over COORD: Dedupe by sourceRoundKey<br/>assertNoActiveRound (local + onchain)<br/>Create salt + priceHash<br/>Classify risk: normal/stress/emergency<br/>Warm backstop standby
COORD->>IDX: POST /pending-price
COORD->>SVR: commit(priceHash)
COORD->>LM: onPriceCommit()
Note over LM: Exclusivity armed<br/>Only LiquidationExecutor<br/>can liquidate
COORD-->>LIQA: SSE: auction frame {roundId, candidates, revealBlock}
COORD-->>LIQB: SSE: auction frame {roundId, candidates, revealBlock}
Note over LIQA,LIQB: Each liquidator:<br/>compute profit per candidate<br/>build signed LiquidationExecutor tx<br/>sign bid hash binding round+reveal+tx
LIQA->>COORD: POST /bid {signedTx, bidHash, bidAmount, roundId}
LIQB->>COORD: POST /bid {signedTx, bidHash, bidAmount, roundId}
Note over COORD: Validate: tx targets LiquidationExecutor<br/>chainId matches, signature binds<br/>round params match
Note over COORD: BID_WINDOW_MS expires<br/>Pick highest valid bid → Liquidator A wins
Note over COORD: Wait for reveal block N+K
COORD->>SVR: reveal(price, salt)
SVR->>LM: update latestRoundData()
COORD->>LE: broadcast winner signed tx
LE->>LM: liquidate(borrower, debtAmount, collateral)
Note over LM: Check exclusivity window<br/>Check health factor at revealed price<br/>Apply close factor + liquidation bonus<br/>Protocol cut → treasury
LM->>LE: seize collateral tokens
LE->>SR: swap(collateral → debtAsset)
SR-->>LE: proceeds
LE-->>LIQA: repay debt + profit
Note over COORD: Winner path complete → round marked done
rect rgb(40, 20, 20)
Note over COORD,BS: FALLBACK PATH (winner fails or no bids)
COORD->>BS: activate backstop
BS->>IDX: GET /positions (fresh state)
BS->>BS: filter active+fresh agents<br/>rank by locality/priority
BS->>BS: parallel race: /standby then /activate
Note over BS: First agent to complete wins
end
The CRE (Chainlink Runtime Environment) is the entry point for all liquidation activity. It operates two modes concurrently: an event-driven path triggered by oracle price updates, and a cron-driven path that sweeps for stale positions that may have become liquidatable without a corresponding price event.
Event-driven trigger pipeline (workflow.ts:65):
SourcePriceOracle.updatePrice()
│
▼ emits SourcePriceUpdated(price1e8)
onSourcePriceLog() decodes log
│
▼
GET /candidates?price1e8={source_price}
│
┌────┴────┐
│ empty │ candidates exist
▼ ▼
stop POST /cre/start-round
{ price1e8, sourceRoundKey }
The sourceRoundKey is a deterministic identifier derived from the source round, used by the coordinator for deduplication. If a network partition causes the same oracle event to trigger CRE twice, the coordinator will reject the second start-round call.
Stale-position cron (workflow.ts:339): CRE independently polls the indexer for positions that have drifted into liquidatable territory without a price event — for example, due to interest accrual increasing a borrower's debt. This ensures the system catches liquidations that the event-driven path would miss.
Guardian emergency mode (workflow.ts:176): CRE can be configured to escalate to a guardian contract under systemic stress conditions, providing a circuit-breaker for cascading liquidation scenarios.
Local simulation entrypoint (cre/source-oracle-oev/main.ts): A stripped-down runner that executes only initSourceWorkflow for development and testing without the full workflow handler chain.
The coordinator owns the entire auction lifecycle. It is the single source of truth for round state and enforces ordering invariants both locally (in-memory + coordinator-state.json) and onchain (via pendingRoundId and pendingExclusiveUntilBlock).
Round state transitions:
Single active round enforcement: The coordinator rejects any /cre/start-round call if pendingRoundId is set either locally or onchain. This prevents two simultaneous auction rounds from competing for the same lending market state.
Risk classification (coordinator.ts:65): Rounds are classified as normal, stress, or emergency based on the number of candidates and the magnitude of the price move. Higher risk classification triggers earlier backstop warming and tighter bid window handling.
Bid window: After BID_WINDOW_MS elapses post-commit, the coordinator locks the bid pool. It selects the highest bid whose validation passes all checks (see section 5.4). It then waits for the reveal block before proceeding to execution.
Persistence: All round state and bids are written to data/coordinator-state.json atomically after each state transition. On restart, the coordinator restores in-progress rounds and resumes from the correct state.
The indexer is an event-sourced position database. It replays all LendingMarket events from genesis (or its last checkpoint) to maintain an accurate map of every borrower's health factor at any given price.
Event sources (indexer.ts:232):
| Event | Effect |
|---|---|
Supplied |
Increase collateral for user |
Withdrawn |
Decrease collateral for user |
Borrowed |
Increase debt for user, add to knownUsers |
Repaid |
Decrease debt for user |
Liquidated |
Reduce both debt and collateral, update firstSeenLiquidatable |
Candidate computation (indexer.ts:179, liquidation.ts:41):
The /candidates endpoint takes a price1e8 parameter (the source trigger price) and returns all borrowers who would have a health factor below 1.0 at that price, along with the estimated liquidation parameters:
For each known user:
Compute HF at price1e8
If HF < 1.0:
Apply close factor → max repayable debt
Apply liquidation bonus → seizable collateral
Estimate net profit
Include in candidate list if profit > threshold
This computation uses the pending/source price before the SVRFeed has been revealed, so it is intentionally approximate. The liquidator's profit solver performs a more precise calculation before bidding.
Pending price tracking (indexer.ts:299): When the coordinator opens a round, it pushes the pending price to /pending-price. The indexer stores this alongside the round ID so that subsequent candidate queries during an open round are tied to the correct price context.
State persistence (indexer.ts:49): Full position state is written to data/indexer-state.json on each update. On restart, the indexer loads this checkpoint and replays only the events it missed while offline, rather than re-indexing from genesis.
Key endpoints:
| Endpoint | Description |
|---|---|
GET /candidates?price1e8= |
Borrowers liquidatable at given price |
GET /positions |
All tracked borrower positions |
GET /stale |
Positions not updated within staleness threshold |
POST /pending-price |
Set pending round price context |
POST /refresh-health |
Force health factor recomputation |
The auction is a sealed-bid, first-price mechanism with a public submission endpoint. Any participant with a valid signed liquidation transaction can compete.
sequenceDiagram
participant COORD as Coordinator
participant LA as Liquidator A
participant LB as Liquidator B
participant LC as External Bidder
COORD->>LA: SSE /auction/stream — roundId, candidates, revealBlock, bidDeadline
COORD->>LB: SSE /auction/stream — roundId, candidates, revealBlock, bidDeadline
Note over LC: polls coordinator API independently
Note over LA: Step 1 — score each candidate<br/>by expected profit at reveal price<br/>select highest-yield borrower
Note over LA: Step 2 — build LiquidationExecutor calldata<br/>sign tx with liquidator EOA key
Note over LA: Step 3 — compute bid hash<br/>keccak256(roundId, revealBlock, bidAmount, txHash)<br/>sign bid hash with EOA
Note over LB: same three steps in parallel
Note over LC: same three steps in parallel
LA->>COORD: POST /bid — signedTx, sig, bidAmount, roundId, revealBlock
LB->>COORD: POST /bid — signedTx, sig, bidAmount, roundId, revealBlock
LC->>COORD: POST /bid — signedTx, sig, bidAmount, roundId, revealBlock
Note over COORD: Validate — recover signer from sig<br/>tx must target LiquidationExecutor<br/>chainId must match config<br/>bid hash binds round + reveal + tx<br/>roundId + revealBlock match active round
Note over COORD: BID_WINDOW_MS expires<br/>sort valid bids descending by amount<br/>winner = highest valid bid
COORD-->>LA: selected as winner — proceed to reveal block
Bid validation (coordinator.ts:233) checks four properties:
- The ECDSA signature recovers to the claimed bidder address
- The decoded transaction targets
LiquidationExecutor(not an arbitrary contract) - The
chainIdin the signed transaction matches the coordinator's configured chain - The bid hash binds
roundId,revealBlock,bidAmount, and the transaction hash — preventing a winning bid from being replayed in a different round or with a different transaction
These validation rules make it impossible to win an auction with a bid that doesn't correspond to a real, executable liquidation transaction on the correct chain and round.
Once the reveal block is reached, execution happens in a tight sequence. The coordinator sends SVRFeed.reveal() first, updating the lending market price, then immediately broadcasts the winner's pre-signed transaction.
Exclusivity window: LendingMarket.onPriceCommit() arms a flag that restricts liquidate() calls to the LiquidationExecutor address until the window expires. This gives the auction winner guaranteed exclusivity — no other liquidator can front-run the winner's transaction during this period. After the window, public liquidation is permitted, allowing anyone to liquidate if the winner fails and the backstop also fails.
Treasury cut: The protocol fee is collected inside LendingMarket.liquidate() as a percentage of the seized collateral, before the remainder is passed to LiquidationExecutor. This happens atomically in the same transaction as the liquidation, making it impossible to extract value without paying the fee.
ERC-8004 is an agent identity registry standard. The registry watcher maintains a live directory of all registered liquidation agents, continuously updated as agents register, update their metadata URI, or go offline.
Event consumption (registry-watcher.ts:270):
The watcher subscribes to two events on AgentIdentityRegistry:
Registered(agentId, metadataURI)— a new agent has registeredURIUpdated(agentId, newURI)— an existing agent has changed its metadata endpoint
On each event, the watcher fetches the /metadata endpoint at the agent's URI to retrieve capability declarations, supported chains, and contact information.
Health probing (registry-watcher.ts:124, registry-watcher.ts:287): The watcher periodically re-probes all known agents to keep lastSeen timestamps fresh. An agent that fails probing is marked inactive and will not be selected by the backstop until it recovers and is re-probed successfully.
Output: All discovered and probed agents are written to data/agentDirectory.json with the following fields per entry:
agentId— registry identitymetadataURI— current endpointcapabilities— declared agent capabilities (must includeliquidation)lastSeen— timestamp of last successful probeactive— boolean health statuspriority— declared execution priority
The backstop is the system's last line of defense against bad debt. It activates when the primary auction winner fails — either because their transaction reverted (e.g., insufficient DEX liquidity at the swap router) or because no valid bids were submitted before the bid window closed.
Agent execution (liquidation_agent.py:159, liquidation.py:48):
Each Python agent implements two HTTP endpoints: /standby (prepare resources, validate position is still liquidatable) and /activate (execute). The liquidate_borrower action tool performs three steps atomically:
- Compute safe repay amount based on current on-chain health factor
- Approve the debt token for
LendingMarket - Call
LendingMarket.liquidate()directly
Agents interact with the lending market directly, without going through LiquidationExecutor. This bypasses the exclusivity window restriction (which has expired by the time backstop activates) and does not route through the swap router, trading some capital efficiency for simplicity and reliability.
The five contracts in this system form a clean dependency graph. No contract calls upstream in the flow — information and value move in one direction from oracle to settlement.
Key invariants enforced on-chain:
SVRFeed.reveal()verifies thatkeccak256(price, salt) == storedHashbefore updating the price feed. A coordinator that commits a hash cannot later reveal a different price.LendingMarket.liquidate()checksmsg.sender == liquidationExecutorduring the exclusivity window. The exclusivity window is set whenonPriceCommit()is called and expires atpendingExclusiveUntilBlock.LendingMarket.liquidate()readsSVRFeed.latestRoundData()at call time — not at commit time, not at bid time. The revealed price is the authoritative price for all liquidation math.LiquidationExecutorrequires the caller to have pre-approved sufficient debt token. The winning liquidator's signed transaction includes this approval, enforcing that the liquidator bears the debt repayment.
Every service in the offchain stack is designed to survive restarts without data loss or state inconsistency.
Coordinator recovery (coordinator.ts:56, round-store.ts:135):
The coordinator writes all round state transitions and received bids to data/coordinator-state.json atomically (write-to-temp then rename). On startup, it reads this file and classifies each round:
committedrounds with an active reveal block in the future: resume waiting for revealcommittedrounds whose reveal block has passed: immediately attempt reveal + executionrevealedrounds with a pending winner tx: re-broadcast the winner txfailedrounds: activate backstop if backstop has not yet been attempted
This means a coordinator restart in the middle of a live round does not lose the round — it picks up from the last persisted state and continues.
Indexer recovery (indexer.ts:49): The indexer checkpoints its full position state to data/indexer-state.json. On restart, it reads the checkpoint, determines the last processed block number, and replays only the events it missed. For a service that processes all lending market events from genesis, this catch-up mechanism is essential for fast restart times.
Registry watcher recovery (registry-watcher.ts:44): The agent directory is written to data/agentDirectory.json continuously. On restart, the watcher loads the existing directory and re-probes all agents to refresh health status before making the directory available to the backstop.
Atomic writes (persist.ts:12): All three services use the same atomic persistence pattern: write to a .tmp file, then rename() to the target path. This prevents partial writes from corrupting state files on crash.
The coordinator supports two execution modes, selected at startup via the EXECUTION_MODE environment variable.
| Mode | Description | Reveal mechanism | Winner tx broadcast |
|---|---|---|---|
testnet-sim |
Local development and testing | Coordinator calls SVRFeed.reveal() directly |
Coordinator broadcasts via standard RPC |
flashbots |
Mainnet with MEV protection | Reveal + winner tx bundled together | Submitted to Flashbots bundle relay |
Flashbots mode (coordinator.ts:883, Flashbots-mainnet/server.ts:14): In production, the reveal transaction and the winner's liquidation transaction are submitted as an atomic Flashbots bundle. This provides two guarantees: the two transactions land in the same block (preventing any gap where the price is revealed but the liquidation hasn't executed), and the bundle is not visible in the public mempool until inclusion (preventing front-running of the reveal).
The Flashbots-mainnet/server.ts component manages bundle construction, signing with the Flashbots relay authentication key, and submission with retry logic. Failed bundle submissions trigger an immediate fallback to the backstop path rather than attempting a second non-private submission.
The terminal interface is a three-page Rust application that provides live observability into the running system. It communicates exclusively with the coordinator and indexer HTTP APIs.
┌─────────────────────────────────────────────────────────────────────────┐
│ Page 1: Dashboard │
│ ───────────────────────────────────────────────────────────────────── │
│ Service health status for CRE, Coordinator, Indexer, Registry Watcher │
│ Latest round snapshot: ID, state, bid count, winner, reveal block │
│ Activity feed: recent round outcomes │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Page 2: Rounds │
│ ───────────────────────────────────────────────────────────────────── │
│ Loads from coordinator rounds API │
│ Frontend filters: open/pending/failed rows shown separately │
│ Columns: round ID, state, candidates, bid count, winner, profit │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Page 3: Positions │
│ ───────────────────────────────────────────────────────────────────── │
│ Reads indexer /positions │
│ Columns: borrower, collateral value, debt, health factor, status │
│ Status: healthy / at-risk / liquidatable / being-liquidated │
└─────────────────────────────────────────────────────────────────────────┘
Navigation: Number keys 1, 2, 3 switch between pages. ENTER on the dashboard opens the full round view.
Scenario hotkeys (main.rs:88):
| Key | Action |
|---|---|
h |
Trigger healthy scenario script |
s |
Trigger stress scenario script |
c |
Trigger crash scenario script |
These hotkeys call external scripts that manipulate the local testnet state — adjusting prices, borrower positions, and market parameters — to simulate different market conditions against the live running services without requiring manual contract interactions.
- Node.js 18+ and npm
- Python 3.10+
- Rust 1.75+ and Cargo
- A local EVM node (Hardhat, Anvil, or Ganache)
- Access to a Chainlink CRE environment or the local simulation entrypoint
git clone <repository>
cd OEV
# Install offchain service dependencies
cd backend
npm install
# Install Python agent dependencies
cd ../agents
pip install -r requirements.txt
# Build the Rust TUI
cd ../tui
cargo build --releaseThe offchain_stack.sh script starts all services with their default ports and inter-service URLs preconfigured:
cd backend/scripts
./offchain_stack.shTo start services individually for development:
# Indexer (default: port 3001)
cd backend && npx ts-node offchain/indexer/indexer.ts
# Coordinator (default: port 3000)
cd backend && npx ts-node offchain/coordinator/coordinator.ts
# Registry Watcher (default: port 3002)
cd backend && npx ts-node offchain/agents/registry-watcher.ts
# CRE source simulation (local testnet only)
cd backend && npx ts-node cre/source-oracle-oev/main.ts
# TUI
cd tui && ./target/release/tuiFor local development, the full Chainlink CRE runtime is replaced by the source oracle simulation entrypoint, which directly calls SourcePriceOracle.updatePrice() on a local testnet and feeds events into the CRE workflow:
# Start local testnet (Anvil example)
anvil --block-time 2
# Deploy contracts
cd backend && npx hardhat run scripts/deploy.ts --network localhost
# Set environment variables
export RPC_URL=http://localhost:8545
export COORDINATOR_URL=http://localhost:3000
export INDEXER_URL=http://localhost:3001
export CHAIN_ID=31337
# Start offchain stack
./scripts/offchain_stack.sh
# Trigger a price update (will fire the full pipeline)
npx ts-node cre/source-oracle-oev/main.tsAll services are configured via environment variables. The following table covers the primary configuration surface.
| Variable | Service | Description | Default |
|---|---|---|---|
RPC_URL |
All | EVM node WebSocket or HTTP endpoint | — |
CHAIN_ID |
All | Target chain ID for tx validation | — |
COORDINATOR_URL |
CRE, Liquidators, TUI | Coordinator service base URL | http://localhost:3000 |
INDEXER_URL |
CRE, Coordinator, TUI | Indexer service base URL | http://localhost:3001 |
BID_WINDOW_MS |
Coordinator | Milliseconds bids are accepted after round open | 5000 |
EXECUTION_MODE |
Coordinator | testnet-sim or flashbots |
testnet-sim |
FLASHBOTS_RELAY_URL |
Coordinator | Flashbots bundle relay endpoint | — |
LENDING_MARKET_ADDRESS |
All | Deployed LendingMarket contract address | — |
SVR_FEED_ADDRESS |
Coordinator | Deployed SVRFeed contract address | — |
LIQUIDATION_EXECUTOR_ADDRESS |
Coordinator, Liquidators | Deployed LiquidationExecutor address | — |
SOURCE_ORACLE_ADDRESS |
CRE | Deployed SourcePriceOracle address | — |
AGENT_REGISTRY_ADDRESS |
Registry Watcher | Deployed AgentIdentityRegistry address | — |
AGENT_FRESHNESS_MS |
Backstop | Max age of lastSeen before agent excluded | 60000 |
COORDINATOR_PRIVATE_KEY |
Coordinator | Key for signing reveal transactions | — |
LIQUIDATOR_PRIVATE_KEY |
Liquidators | Key for signing bid transactions | — |
| Method | Path | Description |
|---|---|---|
POST |
/cre/start-round |
Open a new auction round (CRE only) |
GET |
/auction/stream |
SSE stream of auction frames for liquidators |
POST |
/bid |
Submit a signed bid for the active round |
GET |
/rounds |
List all rounds with state and outcome |
GET |
/health |
Service health check |
| Method | Path | Description |
|---|---|---|
GET |
/candidates |
Borrowers liquidatable at ?price1e8= |
GET |
/positions |
All tracked borrower positions |
GET |
/stale |
Positions not refreshed within staleness window |
POST |
/pending-price |
Set pending round price context |
POST |
/refresh-health |
Force health factor recomputation for all positions |
| Method | Path | Description |
|---|---|---|
GET |
/metadata |
ERC-8004 capability declaration |
POST |
/standby |
Prepare to execute a specific liquidation |
POST |
/activate |
Execute the prepared liquidation |
The codebase is split across four languages and five distinct service boundaries. Changes to one service rarely require changes to others, since all inter-service communication is HTTP. The most likely areas for contribution are:
New liquidation strategies: The liquidator service computes a profit estimate per candidate and bids accordingly. More sophisticated strategies — multi-hop collateral swaps, flash loan integration, partial liquidations across multiple borrowers in one bid — can be implemented by extending liquidator.ts and the corresponding LiquidationExecutor calldata construction.
Additional agent implementations: Any service that implements the three endpoints /metadata, /standby, and /activate and registers with AgentIdentityRegistry is automatically discovered by the registry watcher and made available to the backstop. New agent implementations in any language are valid.
Extended indexer math: The candidate computation in liquidation.ts uses a simplified close factor and bonus model. More accurate models that account for variable rates, multi-asset collateral, and partial liquidation incentives would improve candidate quality and reduce spurious round opens.
Onchain improvements: SVRFeed.sol and LendingMarket.sol are the most sensitive contracts in the system. Any changes require comprehensive test coverage and should be accompanied by a threat model analysis covering the commit-reveal scheme's assumptions.
To contribute:
- Fork the repository and create a branch from
main - Make changes with accompanying tests
- Ensure all existing tests pass:
npm testfor backend services,cargo testfor the TUI - Submit a pull request with a description of the change and its motivation
This project is licensed under the MIT License.








