Multi-operator keeper infrastructure for Soroban DeFi. Distributed liquidation network for Blend Protocol on Stellar β no single point of failure.
Live: nectarnetwork.fun Β· Twitter Β· GitHub
On Feb 22, 2026, a USTRY/XLM oracle manipulation drained $10.8M from a Blend pool. Two pre-positioned single-operator bots captured nearly all of it β 60 auction fills over 4 hours, one Docker container, one keypair, no fallback. The rest of Stellar DeFi (~$187M TVL) had no coordinated response.
Nectar replaces single-bot liquidation systems with a distributed network of competing keepers, funded by a shared vault.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SOROBAN TESTNET β
β β
β ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββ β
β β KeeperRegistry β β NectarVault β β LiquidationLabβ β
β β register() β β deposit() β β get_positions()β β
β β deregister() β β withdraw() β β new_auction() β β
β β get_keepers() β β draw() β β get_auction() β β
β β pause() β β return_proceedsβ β submit() β β
β ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββ β
β β β β β
βββββββββββββΌβββββββββββββββββββββΌβββββββββββββββββββββΌββββββββββββ
β β β
ββββββββ΄βββββββββββββββββββββ΄βββββββββββββββββββββ΄βββββββ
β OFF-CHAIN (Render) β
β β
β ββββββββββββββββββββββ ββββββββββββββββββββββ β
β β Keeper Alpha β β Keeper Beta β β
β β monitor β detect β β monitor β detect β β
β β β draw β fill β β β draw β fill β β
β β β return proceeds β β β return proceeds β β
β ββββββββββββββββββββββ ββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββ
β FRONTEND (Vercel) β nectarnetwork.fun β
β Next.js 14 Β· SSE live stream Β· REST polling β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Monitor β Each keeper independently polls the pool every 10s for positions with health factor < 1.0
- Detect β When HF drops below 1.0, keeper creates a Dutch auction on-chain
- Draw β Keeper draws USDC capital from the NectarVault
- Fill β Keeper fills the auction (first confirmed transaction wins the race)
- Return β Capital + 10% profit returned to the vault; depositors' shares appreciate
- Compete β The losing keeper handles
ErrAlreadyFilledgracefully β no capital lost
| Service | URL |
|---|---|
| Frontend | nectarnetwork.fun |
| Keeper Alpha API | nectar-keeper-i7x3.onrender.com |
| Keeper Beta API | nectar-keeper-beta.onrender.com |
| Contract | Address | Explorer |
|---|---|---|
| KeeperRegistry | CAWT5HBM25OKGOMJHPFCXWXDWZ7FF436WXRKROTY2VW642FSKLYUKOUB |
View |
| NectarVault | CCXDLRE3IV5225LE3Z776KFB2VWD2MTXOJHAUKFA5RPYDJVOWCMHJ4U4 |
View |
| LiquidationLab | CDOXKPEBRQG3MSDOWBROUVGRO6TTC4NJJPW7GCXCR5WR5SUSMEAFE7Y5 |
View |
| USDC Token (SAC) | CAVBAVD6CZ46FEDKJHBQIJF7EFAZDTRNS65G73QS5ZYI3VK5E2JFPQ4J |
View |
- TVL: $116,003 USDC across 18 depositors
- Keepers: 2 registered operators (alpha + beta)
- Liquidations: 5+ completed, end-to-end on-chain
- Profit model: 10% per successful liquidation returned to vault depositors
nectar-poc/
βββ contracts/
β βββ keeper-registry/ # Soroban (Rust) β operator registration, 9 tests
β βββ nectar-vault/ # Soroban (Rust) β USDC vault + LP shares, 17 tests
β βββ liquidation-lab/ # Soroban (Rust) β Blend-compatible test pool, 12 tests
βββ keeper/ # Go 1.22 β keeper binary
β βββ main.go # Entry point, HTTP API, SSE, keeper loop
β βββ config.go # Env config with validation
β βββ soroban/ # Soroban JSON-RPC client + tx assembly
β βββ blend/ # Pool, positions, auction (Blend-compatible)
β βββ vault/ # Vault draw/return/balance queries
β βββ registry/ # Keeper register/check
βββ frontend/ # Next.js 14 + Tailwind CSS
β βββ app/
β β βββ page.tsx # Home β hero, live log stream, architecture
β β βββ features/ # How It Works β 5 core features explained
β β βββ vault/ # Deposit/Withdraw UI with Freighter wallet
β β βββ performance/ # Live dashboard β depositors, keepers, liquidations
β βββ lib/
β βββ api.ts # REST API client + types
β βββ sse.ts # SSE hook with exponential backoff
β βββ stellar.ts # Freighter wallet integration
βββ scripts/ # Deployment + provisioning scripts
βββ docker-compose.yml # Keeper Alpha + Beta + Frontend
βββ render.yaml # Render.com deployment blueprint
βββ wallets.md # All testnet wallet addresses (public keys)
On-chain registry for keeper operators. Any operator can self-register with a keypair. Admin can pause in emergencies.
| Function | Description |
|---|---|
initialize(admin) |
Set admin, create empty keeper list |
register(keeper, name) |
Register a new keeper operator |
deregister(keeper) |
Remove a keeper from the registry |
get_keepers() |
List all registered keepers |
pause() / unpause() |
Emergency admin controls |
Pooled USDC vault that funds liquidations. Depositors receive LP shares proportional to their deposit. Shares appreciate as keepers return profits.
| Function | Description |
|---|---|
initialize(admin, usdc_token, registry) |
Configure vault with USDC token and registry |
deposit(depositor, amount) |
Deposit USDC, receive LP shares |
withdraw(depositor, shares) |
Redeem shares for USDC at current share price |
draw(keeper, amount) |
Keeper draws USDC for liquidation (must be registered) |
return_proceeds(keeper, amount) |
Return capital + profit after successful liquidation |
balance(user) |
Query user's shares and USDC value |
Blend-compatible pool contract that the Go keeper interacts with directly β same interface as a real Blend pool. Admin can set positions and control auctions for testing.
| Function | Description |
|---|---|
get_reserve_list() |
List reserve assets (XLM, USDC) |
get_reserve(asset) |
Reserve config (collateral/liability factors, rates) |
get_positions(user) |
User's collateral and liability maps |
new_liquidation_auction(user, pct) |
Create Dutch auction for underwater position |
get_auction(type, user) |
Fetch active auction data |
submit(from, spender, to, requests) |
Fill auction (keeper submits fill request) |
The Go keeper needs zero code changes to switch between a real Blend pool and LiquidationLab β just change the BLEND_POOL env var.
The keeper binary is a single Go process that:
- Registers itself on the KeeperRegistry
- Polls the pool every 10s for positions
- Computes health factors using reserve configs and oracle prices
- Creates and fills Dutch auctions when HF < 1.0
- Draws capital from the vault, fills auctions, returns proceeds
- Serves a REST API + SSE stream for the frontend
| Endpoint | Method | Description |
|---|---|---|
/api/state |
GET | Current positions, keepers, events, vault state |
/api/performance |
GET | TVL, depositors, keeper stats, liquidation history |
/api/events |
GET | SSE stream of real-time keeper events |
/metrics |
GET | Prometheus metrics (cycles, liquidations, TVL) |
/healthz |
GET | Health check |
- Dutch Auction Profitability:
lotPct = elapsed/200,bidPct = (200-elapsed)/200. Keeper fills whenlot_value / bid_cost > MIN_PROFIT - Multi-Operator Race: First confirmed tx wins. Loser gets
ErrAlreadyFilledβ capital returned safely - Vault Capital Safety:
return_proceedsonly called on fill success orErrAlreadyFilled. Hard failures propagate error without returning - SSE Client Limit: Max 100 concurrent connections, 503 if exceeded
- Graceful Shutdown:
SIGTERM/SIGINTβ drain in-flight cycle β clean exit - XDR Encoding: ScMap keys sorted lexicographically (Soroban requirement),
ScVal.Vecis**xdr.ScVec(double deref)
Next.js 14 with Tailwind CSS. Dark theme, monospace design.
| Page | Route | Description |
|---|---|---|
| Home | / |
Hero with live SSE log stream, problem stats, architecture diagram, keeper registry, position monitor |
| Features | /features |
5 core features explained with getting-started guides |
| Vault | /vault |
Freighter wallet integration, deposit/withdraw, live balance queries |
| Performance | /performance |
18 depositors, 2 keepers, vault TVL, liquidation history |
The vault page integrates with Freighter wallet:
- Detect Freighter extension
- Connect and read balances (XLM + USDC)
- Submit deposit/withdraw transactions to Soroban
- Query vault share balances on-chain
- Link to Stellar Expert for transaction verification
- Go 1.22+
- Rust +
wasm32-unknown-unknowntarget - Node.js 18+
- Stellar CLI (for contract deployment)
cargo build --release --target wasm32-unknown-unknown
cargo test # 38 tests across 3 contracts# Generate and fund wallets
stellar keys generate admin --network testnet
# Deploy contracts
stellar contract deploy --wasm target/.../keeper_registry.optimized.wasm --source admin --network testnet
stellar contract deploy --wasm target/.../nectar_vault.optimized.wasm --source admin --network testnet
stellar contract deploy --wasm target/.../liquidation_lab.optimized.wasm --source admin --network testnetcd keeper
cp ../.env.example .env # configure with your contract IDs + keypair
go run .cd frontend
npm install && npm run dev
# β http://localhost:3000docker-compose up
# keeper-alpha: localhost:8080
# keeper-beta: localhost:8081
# frontend: localhost:3000| Variable | Required | Description |
|---|---|---|
KEEPER_SECRET |
yes | Stellar secret key (S...) for the keeper operator |
KEEPER_NAME |
no | Display name (default: keeper-alpha) |
REGISTRY_CONTRACT |
yes | KeeperRegistry contract ID |
VAULT_CONTRACT |
yes | NectarVault contract ID |
BLEND_POOL |
yes | Pool contract ID (Blend or LiquidationLab) |
POLL_INTERVAL |
no | Seconds between cycles (default: 10, range: 3-300) |
MIN_PROFIT |
no | Minimum lot/bid ratio to fill (default: 1.0) |
KNOWN_DEPOSITORS |
no | Comma-separated G-addresses for performance page |
API_PORT |
no | HTTP API port (default: 8080) |
# Rust contract tests (38 total)
cargo test -p keeper-registry # 9 tests
cargo test -p nectar-vault # 17 tests
cargo test -p liquidation-lab # 12 tests
# Go keeper tests (30+ total)
cd keeper && go test -race -count=1 ./...
# unit tests, integration tests, stress tests, benchmarks
# Frontend build
cd frontend && npm run build- Depositor TTL: 535,680 ledgers (~30 days) β prevents share loss from expiration
- Division-by-zero guard: Withdraw checks
total_shares > 0 - Config validation: Poll interval bounds [3, 300], min profit > 0, env parse errors crash fast
- Capital safety: Vault draw only returns proceeds on successful fill or
ErrAlreadyFilled - Deadlock prevention: Separate mutex for SSE subscriber list vs data fields
- SSE limit: Max 100 concurrent clients, 503 rejection
- Graceful shutdown: Signal handling with in-flight cycle drain
Both keepers deploy via render.yaml blueprint:
- Docker builds from
keeper/Dockerfile - Free tier with auto-sleep/wake
- Auto-deploy on git push
Next.js deployed to Vercel with output: "standalone":
- Custom domain: nectarnetwork.fun
- Environment variables for contract addresses and API URL
MIT