A beginner-friendly MEV liquidation bot for Jupiter Lend on Solana, built in Rust.
This bot is for educational purposes only. MEV extraction carries risks:
- You could lose funds if transactions fail
- Competing bots may frontrun your transactions
- Always test on devnet first
- Never commit private keys to version control
- Rust - Install from rustup.rs
- Solana CLI - Install from docs.solana.com
- A Solana wallet with SOL for live transaction fees
- Copy the example configuration:
cp config.example.json config.json
- Edit
config.jsonwith your RPC endpoint and vaults. - Keep
simulation_modeset totruewhile testing. - For live mode, provide a valid base58
private_keyorSOLANA_PRIVATE_KEYenvironment variable. Never commit that key. - Build and run:
cargo build --release cargo run --release
jupiter-liquidation-bot/
├── Cargo.toml
├── config.json # Local config; keep private keys out of version control
├── config.example.json
├── src/
│ ├── main.rs # Entry point and task orchestration
│ ├── config.rs # Configuration and safety validation
│ ├── jupiter.rs # Jupiter Lend interaction
│ ├── pyth.rs # Price feeds
│ ├── monitor.rs # Polling account monitoring
│ ├── ws_monitor.rs # WebSocket account monitoring
│ ├── liquidation.rs # Simulation and live execution
│ ├── jito.rs # Jito bundle submission
│ └── types.rs # Shared data structures
└── README.md
| Option | Type | Required | Description | Default |
|---|---|---|---|---|
rpc_url |
string | Yes | Solana RPC endpoint | Mainnet public RPC |
ws_url |
string | No | WebSocket endpoint; polling is used when absent or after WS retries fail | null |
jito_block_engine_url |
string | Live | Jito endpoint for live bundle submission | Mainnet Jito |
private_key |
string | Live | Base58 wallet key; simulation can use a temporary signer | null |
simulation_mode |
boolean | No | Build, sign, and RPC-simulate without submitting to Jito | true |
min_health_factor |
float | Yes | Detection threshold from 0.0 to 1.0 | 1.0 |
target_health_factor |
float | No | Only send positions below this threshold to execution | 0.95 |
max_liquidation_amount |
float | Yes | Maximum liquidation amount per opportunity | 1.0 |
jito_tip_lamports |
integer | Yes | Live Jito tip in lamports | 100000 |
paper_trade_path |
string | No | JSONL path for persisted delinquent observations | data/paper_trades.jsonl |
alerts |
object | No | Telegram/Discord alert configuration | Disabled |
health_check |
object | No | RPC and monitor health checks | Enabled |
vaults |
array | Yes | Jupiter Lend VaultConfig PDA addresses to monitor |
Required |
vaults[].addressis the on-chainVaultConfigPDA (seeds"vault_config"+vault_idlittle-endian, programjupr81YtYssSyPt8jbnGuiWon5f6x9TcDEFxYe3Bdzi) — a 219-byte account, not the old fabricated addresses. Find real ones withsolana program accounts jupr81YtYssSyPt8jbnGuiWon5f6x9TcDEFxYe3Bdzi(pick the 219-byte accounts) or derive them from the vault ids. WebSocket monitoring derives the config from each position'svault_idautomatically; a wrong entry only disables the polling fallback for that vault.
Configuration precedence is:
--simulateor--livecommand-line flagSIMULATION_MODE=true|falseenvironment variablesimulation_modeinconfig.json
Simulation builds and signs the same transaction shape as live mode, calls Solana simulateTransaction, records separate simulation metrics, and never calls Jito or submits a transaction. Every configured Telegram or Discord simulation alert is prefixed with [SIMULATION].
Examples:
cargo run --release -- --simulate
SIMULATION_MODE=true cargo run --release
cargo run --release -- --liveLive mode refuses to start without a valid private key. Simulation mode may generate a temporary signer when no key is configured; this does not give that signer any authority over a submitted transaction because simulation never submits.
The dashboard exposes every successfully decoded position through /api/monitored and the monitored WebSocket event. Each row is keyed by owner plus vault and includes health, collateral, debt, liquidation thresholds, target eligibility, and estimated gross/cost/net reward values.
The monitored view uses a 120-second last-seen policy and emits monitored_removed when a position becomes stale. UNKNOWN means the position was decoded but valuation was unavailable; it is not treated as liquidation-eligible.
Rewards are explicitly estimates. Gross reward is the approximate liquidation bonus, estimated cost is base gas plus the configured Jito tip, and net reward is gross less that cost. In simulation mode, these values are not realized profit and no transaction is submitted.
Delinquent observations are deduplicated and persisted as JSONL at paper_trade_path. The dashboard exposes recent records through /api/paper-trades and the Paper-trade history panel; healthy positions are not recorded. The ledger is local measurement data and is excluded from version control by default.
Telegram uses telegram_token and telegram_chat_id. Discord uses a bot token and channel ID:
"alerts": {
"enabled": true,
"telegram_token": "YOUR_TELEGRAM_BOT_TOKEN",
"telegram_chat_id": "YOUR_TELEGRAM_CHAT_ID",
"discord_bot_token": "YOUR_DISCORD_BOT_TOKEN",
"discord_channel_id": "YOUR_DISCORD_CHANNEL_ID"
}The Discord bot needs VIEW_CHANNEL and SEND_MESSAGES permissions in the target channel. Bot API failures are reported instead of being treated as successful delivery.
The bot decodes the real Jupiter Lend Vaults program accounts (jupr81YtYssSyPt8jbnGuiWon5f6x9TcDEFxYe3Bdzi):
- Positions are 71-byte zero-copy accounts (PDA
"position" + vault_id + position_id) containingvault_id,nft_id,position_mint,tick,supply_amountanddust_debt_amount. They store no wallet or vault address. - Vault configs are 219-byte zero-copy accounts (PDA
"vault_config" + vault_id) holding the liquidation threshold/penalty and the supply/borrow token mints. Theaddressentries inconfig.jsonmust be theseVaultConfigPDA addresses. - Debt is derived from the tick (
debt = floor((supply + 1) × 1.0015^tick) + 1 + dust), mirroring on-chain accounting.
Flow:
- Monitor: Watch real Jupiter Lend positions over WebSocket or polling.
- Detect: Calculate health factors from real on-chain amounts and prices; retain fresh delinquent positions for the dashboard.
- Filter: Send only positions below
target_health_factorto the executor. - Simulate or execute: Simulation calls RPC simulation; live mode submits through Jito and confirms the signature.
- Track: Keep simulation and live metrics separate and expose monitoring mode through the API.
Health factor formula:
Health Factor = (Collateral Value × Liquidation Threshold) / Debt Value
< 1.0: delinquent and potentially liquidatable>= 1.0: healthy
⚠️ Monitoring is real; liquidation submission is not yet. Building the on-chainliquidateinstruction requires discovery of branch/tick/oracle-source and liquidity accounts, which is a follow-up. The executor therefore reports a clear "not implemented" error instead of simulating a fabricated transaction. Keepsimulation_modeon.
Run the Rust checks from jupiter-liquidation-bot:
cargo fmt -- --check
cargo check
cargo test- Check the RPC endpoint and rate limits.
- Verify vault addresses and account data.
- Ensure the simulated signer and required token accounts are valid for the transaction.
- Confirm live mode has a valid private key.
- Check Jito Block Engine status and tip amount.
- Verify the transaction format.
This is an explicit status, not necessarily an error. The monitor uses polling when no ws_url is configured or after WebSocket reconnection attempts are exhausted.
- Never commit private keys.
- Use a burner wallet with only necessary funds.
- Keep simulation enabled until the complete path is understood.
- Monitor alert credentials and Discord permissions.
- Use a dedicated RPC endpoint for reliable production monitoring.
MIT License - Use at your own risk!