All articles
Risk·March 31, 2026·7 min read

Circuit Breakers for Solana Bots: Halt Trading on Depeg & Oracle Gaps

Build a trading bot circuit breaker on Solana that halts on Pyth/Switchboard stale feeds, stablecoin depegs, and Jupiter route divergence before the kill-switch.

A depeg doesn't announce itself. One block USDC is trading at 0.9998 against USDT on Orca, the next it's at 0.94 because a redemption queue backed up and a whale dumped into thin liquidity. If your bot is holding USDC-quoted positions and marking off a feed that hasn't caught up yet, it will happily size into the crater. The kill-switch — the big red "flatten everything and disconnect" button — is the last line of defense. Circuit breakers are the ones that should fire first, automatically, before a human is even looking at the screen.

On Solana this matters more than on a centralized venue, because you're stitching price truth together from several independent sources: a Pyth or Switchboard oracle for mark price, an AMM or Jupiter route for execution price, and sometimes a CEX reference for a sanity check. Any one of those can lie to you for a few slots. A circuit breaker is the logic that notices the disagreement and halts new entries until the sources reconcile.

What a circuit breaker actually is (and isn't)

A kill-switch is binary and terminal: it cancels working orders, closes positions, and stops the process. You pull it once, and recovery is manual. A circuit breaker is graded and reversible. It moves the bot between states — TRADING, REDUCE_ONLY, HALTED — based on live signals, and it can lift itself once conditions normalize. That distinction is the whole point. You want dozens of small, cheap halts that auto-recover, so the expensive manual kill-switch stays a rare event.

Three failure modes on Solana are worth wiring breakers for specifically:

  • Stale oracle feeds — the Pyth publish slot lags, or a Switchboard aggregator stops updating.
  • Stablecoin depeg — USDC, USDT, or PYUSD drift off 1.00 by more than your tolerance.
  • Route price divergence — the Jupiter quote for your intended size disagrees with the oracle mark, which usually means either thin liquidity or an oracle that's behind.

Breaker 1: Pyth and Switchboard staleness

Pyth prices carry a publish time and a confidence interval, and both are load-bearing. The naive check is "is price_account.timestamp older than N seconds?" — but wall-clock time is the wrong clock. Solana runs on slots, and RPC clock skew will bite you. Compare the publish slot against the current slot instead, and budget in slots: at roughly 400ms per slot, a 5-slot lag is about 2 seconds. For a fast strategy, halt entries once the feed is more than 8–10 slots stale.

Don't ignore the confidence band. Pyth gives you a conf value alongside the price; when conf / price blows past your threshold (say 0.5% for a major, tighter for stables), the oracle is telling you it isn't sure. Treat that as a soft breaker — go REDUCE_ONLY rather than fully halting.

fn oracle_ok(p: &PythPrice, now_slot: u64) -> BreakerState {
    let lag = now_slot.saturating_sub(p.publish_slot);
    let conf_bps = (p.conf as f64 / p.price as f64) * 10_000.0;
    if lag > 10 { BreakerState::Halted }        // feed is stale
    else if lag > 4 || conf_bps > 50.0 { BreakerState::ReduceOnly }
    else { BreakerState::Trading }
}

Switchboard behaves differently. Its on-demand feeds update when someone crank-pulls them, so a quiet feed isn't necessarily broken — it just hasn't been refreshed. If you rely on Switchboard, either run your own crank on a schedule or subscribe to the surge/streaming variant, and treat "no update in X slots on a feed I expect to move" as your staleness signal. Getting these thresholds right is exactly the kind of thing a second set of eyes catches, which is why we push clients toward an independent audit of their oracle-handling code before it goes anywhere near real size.

Breaker 2: Stablecoin depeg

The mistake here is checking a stablecoin against its own oracle. If USDC depegs and the Pyth USDC feed is honest, the feed reports 0.94 and your peg check reads 0.94 as "correct." You've measured nothing. Cross-check the quote asset against a second independent source. A cheap, effective version: compare the on-chain USDC/USDT AMM mid against 1.00. Two stablecoins rarely depeg in the same direction at the same magnitude, so their ratio drifting past, say, 30–50 bps is a strong depeg signal.

The subtler risk is your own PnL accounting. If your book is denominated in USDC and USDC drops to 0.97, your dollar exposure just changed by 3% with zero fills. Bots that mark everything "in USDC = in dollars" silently mis-size for the entire depeg window. When the peg breaker trips, freeze new entries and re-mark open positions using the depegged quote value, so your risk engine sees the real number. This is a close cousin of the pre-settlement resolution risk that hits Polymarket bots, where the asset you thought was worth a dollar quietly stops being one.

Breaker 3: Jupiter route divergence

Your oracle says SOL is 148.20. Jupiter quotes your 5,000-USDC buy at an effective 151.90. That 2.5% gap isn't slippage in the usual sense — it's a divergence between reference price and executable price, and it means one of two things: the route is thin for your size, or the oracle is stale and the AMM already repriced. Either way, entering blind is a mistake.

Wire the breaker to the size-adjusted quote, not the spot mid. Pull a Jupiter quote for your actual intended notional, compute the effective price including price impact, and compare against the oracle mark:

divergence_bps = |jup_effective_price - oracle_mark| / oracle_mark * 10_000
if divergence_bps > entry_threshold: block_entry()

Set the entry threshold tighter than your exit threshold. You want to be picky about getting in and lenient about getting out — an emergency exit shouldn't be blocked just because the book got thin. The mechanics of measuring effective price against impact overlap heavily with ordinary execution guardrails, and the slippage and price-impact guardrails for Jupiter swap bots cover the quoting side in more depth than fits here.

Wiring it together as a state machine

Individual checks are easy. The hard part is composition: what happens when two breakers disagree, and how do you avoid flapping between states every slot? A few rules that have held up in production:

  • Most restrictive wins. If the oracle breaker says REDUCE_ONLY and the depeg breaker says HALTED, you're HALTED. No voting, no averaging.
  • Asymmetric hysteresis. Trip fast, recover slow. Halt the instant a threshold breaks, but require the signal to stay clean for a cooldown — 30 to 60 slots — before lifting. This kills the oscillation where a feed hovers right at your boundary and you machine-gun in and out of the market.
  • REDUCE_ONLY is your friend. Most events don't warrant a full stop. Blocking new entries while still allowing exits keeps you able to de-risk into the exact conditions that tripped the breaker.
  • Log every transition with the trigger reason and the values that caused it. When you're staring at a halt at 3am, "HALTED: pyth_lag=14 slots" is the difference between a two-minute diagnosis and an hour of guessing.

One gotcha that catches people: a halted bot with open positions is not automatically safe. If you halt entries during a depeg but leave a leveraged perp open, your liquidation risk is still live. The breaker layer has to coordinate with position management — halting new risk is not the same as reducing existing risk. This is where breaker design bleeds into sizing, and the volatility-targeted position sizing used in Hyperliquid bots pairs naturally with it: smaller positions going in means less to unwind when a breaker fires mid-trade.

Breakers also need to be visible. A halt that only exists in a log file is a halt nobody trusts. Surface the current state, the active trigger, and the raw feed values on a live trading dashboard so an operator can see at a glance whether the bot halted for a real reason or a flaky RPC. And plan to tune thresholds as liquidity and feed behavior shift — what's a sane divergence band in a calm market is far too tight during a volatility spike, which is one reason breaker thresholds belong under ongoing maintenance rather than set-and-forget.

Start with the three breakers above, test them against replayed depeg and stale-feed data before you trust them live, and treat every manual kill-switch pull as a bug report: something should have caught it sooner. If you'd rather have that safety layer designed and stress-tested against real Solana feed failures from the start, that's the kind of build our code review and audit work exists to cover.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#solana#risk-management#oracles#circuit-breaker#pyth