Skip to content
Open app

Rebalancer

The rebalancer decides where each vault's USDC should sit and how much of the way to move this cycle. It is deterministic: the same rates, balances and memory always give the same decision. Moving costs money and spreads between stablecoin venues are small, so most cycles end in a hold. That is the design working, not the keeper idling.

On this page

One cycle#

The keeper runs a cycle every 10 minutes. For each tier, Bedrock then Rapids:

  1. Read rates

    Fetch every venue's APY, TVL and utilisation from the venue APIs or DefiLlama, cached for 5 minutes. See rate sources.

  2. Run the risk sentinel

    Classify each venue as ok, caution or pause, which can lower its cap or set it to zero. See Risk sentinel.

  3. Read the vault

    Read idle, total assets and every strategy position from the chain.

  4. Smooth, solve and plan

    Update the smoothed APYs, solve target weights and decide which moves are worth making this cycle.

  5. Save memory, then execute

    Persist the planner's memory (cooldowns, turnover, smoothed rates) before sending anything. Then send one deallocate or allocate per venue, deallocations first, stopping at the first failure.

  6. Mirror rates and log

    Write every venue's live APY into its devnet reserve, then append the decision and the new targets to the log.

A cycle never overlaps the previous one, and a failure in one tier does not stop the other. Saving memory first means a crash in the middle of a rebalance can only make the keeper more cautious after a restart; it never repeats a move.

Smoothed rates#

The planner never acts on a single spot sample. It uses an exponential moving average of each venue's APY:

  • The average has a 12-hour half-life.
  • One observation can pull the average by at most 2 APY points. A short spike barely registers; a sustained change comes through in about a day.
  • A venue seen for the first time, or after a restart without memory, starts from its 7-day median rather than the spot rate.

Each decision records both the smoothed and the spot APY of every venue, so you can see how far apart they are. The risk sentinel works on spot rates, so smoothing never delays an exit.

Targets#

The optimizer computes where each tier should ideally sit. Each venue gets a risk-adjusted score, then the weights solve:

score = APY − λ × risk score² − utilisation penalty maximise Σ w × score − (γ ÷ 2) × Σ w²
w = weight of each venue as a fraction of the vault.

subject to:

  • each weight between its floor and its cap (the lowest of the on-chain cap, the sentinel cap and the 35% concentration limit);
  • all weights together at most 95%, leaving the 5% idle buffer;
  • each non-Solana chain at most 50%, and all cross-chain venues together at most 50%;
  • the best three eligible venues at least 5% each, so a vault never piles into one or two.

The quadratic term is a concentration penalty: the more weight a venue already has, the less the next dollar there is worth. The solution is a water-filling whose level is found by bisection, with chain groups pinned to their caps when needed.

ParameterBedrockRapids
λ, risk aversion (APY points per risk score²)0.080.025
γ, concentration penalty64
Idle buffer5%5%
Per-chain cap / cross-chain cap50% / 50%50% / 50%
Minimum venues with a floor weight3 at 5%3 at 5%
Utilisation penaltyLinear above 90%, 2 points at 100%Same
From TIER_PARAMS in apps/keeper/src/optimizer.ts.

With λ = 0.08, a Bedrock venue with risk score 3 is charged 0.72 points against 0.32 for a score of 2, so it must pay 0.4 points more to compete. Rapids charges less than a third as much, which is what lets it hold riskier venues. The latest targets are served at /targets.

Planning moves#

Targets say where to go. The planner decides how much of the way is worth moving now, in this order:

  1. Safety exits. Any position above its cap, whether the on-chain cap or a lower one from the sentinel, is cut to the cap at once and the excess goes to idle. Exits ignore every churn limit below.
  2. Frozen venues. A venue the sentinel flags now, or flagged in the last 24 hours, can lose funds but never gain them, so the sentinel and the optimizer cannot fight over a spike.
  3. Deploy idle. Idle above the 5% buffer goes into under-target venues, best score first, as long as the expected yield at least pays for the move. Deploys are one-way and do not count toward turnover.
  4. Venue-to-venue moves. Only when every condition below holds. Each move closes at most half of a venue's gap.
ConditionDefault
One end of the move is outside the drift bandLarger of 1.5% of the vault, 15% of the target and the minimum move
The move is at least the minimum sizeLarger of 100 USDC and 0.5% of the vault
No reallocation in the vault recently60 minutes
Neither venue moved recently6 hours
The cost check passesGain at least 2× cost and 1 USDC more than it
Turnover budget left10% of the vault per cycle, 20% per rolling 24 hours
A venue above the 35% concentration limitTrimmed back to the limit even if the cost check fails

Finally the moves are netted to one deallocate or allocate per venue, deallocations first, and handed to the executor. The executor clamps each allocation to the live on-chain headroom under the cap, with a 5 bp margin, and skips anything under 1 USDC.

The cost check#

A venue-to-venue move must pay for itself within the holding period, even if the spread shrinks a little:

gain = amount × (spread − 0.5) ÷ 100 × 30 ÷ 365 × (1 − performance fee) cost = cost of leaving the source + cost of entering the destination
Spread is the difference in risk-adjusted score between the two venues, in APY points.
LegFixedProportional
Solana venue0.01 USDC1 bp
Cross-chain venue (CCTP)5 USDC2 bp
IdleFreeFree
Estimates used to judge whether a move pays, not fees actually charged. Every value is an environment variable; see Tuning.

A real example from the decision log on 8 October 2026. The planner considered moving 14,896 USDC from Jupiter Lend to Aave v3 on Base, and held:

rationale.held
Jupiter Lend → Aave v3 14,896 USDC: -0.09% spread gains -6.85 USDC over 30d vs 9.48 cost (needs 2x, after a 0.5% margin)

The cost is 1.50 USDC to leave Jupiter (0.01 + 1 bp) plus 7.98 USDC to enter Aave across CCTP (5 + 2 bp). After risk adjustment Aave scored slightly lower than Jupiter, so the gain was negative before the margin and the move was never close. A deploy from idle skips the margin and only needs the gain to cover its cost.

Coming with the next keeper release

The next keeper prices CCTP legs by destination: 0.50 USDC plus 2 bp to an L2 such as Base or Arbitrum (REBAL_TX_COST_L2_USDC), and 5 USDC plus 2 bp to Ethereum mainnet, where gas dominates. The same move would then cost about 4.98 USDC.

The decision log#

Every cycle writes one decision per tier, whether it moved or not. The app's Rebalancer view shows them; the keeper serves the latest 200 at /decisions.

FieldMeaning
kindhold, rebalance, or derisk (at least one sentinel exit)
summaryOne line for people, such as the move made or the main reason for holding
movesExecuted flows as from → to → amount, paired across venues; leftovers go to or come from idle
expectedApyBefore / AfterNet APY of the vault on smoothed rates, before and after the moves
sentinelThe sentinel's summary verdict, score and note for the tier
signaturesSolana transaction signatures of the executed moves
rationale.legsEach planned leg: derisk, breach, concentration, yield or deploy, with spread, gain, cost and a note
rationale.heldWhy other candidate moves were not made, most relevant first (up to 5)
rationale.frozenVenues that could not receive funds this cycle
rationale.turnover24hUsdVenue-to-venue volume in the last 24 hours, and its cap
rationale.smoothedApy / spotApyThe rate the plan used and the latest sample, per venue
GET/decisions

How well it works#

On 8 October 2026, 197 of the 200 decisions the keeper served were holds and the other 3 were sentinel exits; there was no yield-driven reallocation in that window. Spreads between blue-chip USDC venues are usually smaller than the 0.5-point margin plus the cost of moving.

A backtest replays historical rates through the real cycle (the same sentinel, smoothing and planner, with a paper executor). Over 90 days from 10 July 2026, with 6-hour steps and 1,000,000 USDC per tier:

  • Bedrock
    Realised APY
    4.16%
    Static day-one mix
    4.19%
    Oracle bound
    4.65%
    Moves
    38
    Turnover per year
    1.22×
    Est. costs
    $267
  • Rapids
    Realised APY
    7.39%
    Static day-one mix
    7.41%
    Oracle bound
    7.76%
    Moves
    9
    Turnover per year
    0.43×
    Est. costs
    $156
The oracle always sits in the best spot rates within the caps, with no costs and no risk view, so it cannot be reached.

The policy captures about 89% (Bedrock) and 95% (Rapids) of the oracle bound and lands within 0.03 points of holding its first allocation, while keeping the ability to exit a venue on a risk signal. Parameter sweeps moved realised APY by at most 0.03 points, with the direction changing between windows, so the defaults are unchanged.

Coming with the next keeper release

The backtest (pnpm --filter @aqueduct/keeper backtest) ships with the next keeper release, together with the sentinel changes it was run with: hysteresis, data holds and chain-cap exits. See Risk sentinel.

Tuning#

Every planner parameter can be overridden with an environment variable on the keeper. None are set in production. Sentinel thresholds and optimizer parameters are constants in the code.

  • REBAL_RATE_HALF_LIFE_HOURS
    Default
    12
    Meaning
    Half-life of the smoothed APYs the planner uses
  • REBAL_RATE_OUTLIER_CLAMP_PCT
    Default
    2
    Meaning
    Most one observation can move the smoothed APY, in APY points
  • REBAL_HOLDING_PERIOD_DAYS
    Default
    30
    Meaning
    How long a move is expected to keep paying
  • REBAL_UNCERTAINTY_MARGIN_PCT
    Default
    0.5
    Meaning
    Subtracted from every venue-to-venue spread
  • REBAL_MIN_GAIN_COST_RATIO
    Default
    2
    Meaning
    Expected gain must be at least this multiple of the cost
  • REBAL_MIN_NET_GAIN_USDC
    Default
    1
    Meaning
    …and beat the cost by at least this much
  • REBAL_TX_COST_SOLANA_USDC
    Default
    0.01
    Meaning
    Fixed cost of a Solana leg
  • REBAL_TX_COST_CROSSCHAIN_USDC
    Default
    5
    Meaning
    Fixed cost of a cross-chain (CCTP) leg
  • REBAL_FRICTION_BPS_SOLANA
    Default
    1
    Meaning
    Proportional cost of a Solana leg, bps
  • REBAL_FRICTION_BPS_CROSSCHAIN
    Default
    2
    Meaning
    Proportional cost of a cross-chain leg, bps
  • REBAL_MIN_MOVE_USDC / REBAL_MIN_MOVE_BPS
    Default
    100 / 50
    Meaning
    Smallest move: the larger of 100 USDC and 0.5% of the vault
  • REBAL_DRIFT_ABS_BPS / REBAL_DRIFT_REL_PCT
    Default
    150 / 15
    Meaning
    Drift band: 1.5% of the vault or 15% of the target
  • REBAL_STEP_FRACTION
    Default
    0.5
    Meaning
    Share of a venue's gap closed per cycle
  • REBAL_VENUE_COOLDOWN_HOURS
    Default
    6
    Meaning
    Minimum time between moves touching the same venue
  • REBAL_GLOBAL_COOLDOWN_MINUTES
    Default
    60
    Meaning
    Minimum time between venue-to-venue reallocations
  • REBAL_REENTRY_COOLDOWN_HOURS
    Default
    24
    Meaning
    No new funds for a venue after the sentinel flags it
  • REBAL_MAX_CYCLE_TURNOVER_BPS
    Default
    1000
    Meaning
    Venue-to-venue volume per cycle: 10% of the vault
  • REBAL_MAX_DAILY_TURNOVER_BPS
    Default
    2000
    Meaning
    Venue-to-venue volume per rolling 24h: 20% of the vault
  • REBAL_MAX_VENUE_BPS
    Default
    3500
    Meaning
    Concentration limit per venue in the targets: 35%

To minimise transactions rather than maximise yield, REBAL_RATE_HALF_LIFE_HOURS=24 and REBAL_MIN_GAIN_COST_RATIO=3 cut turnover by about 20% in the backtest with no measurable yield cost.

Written against apps/keeper/src/cycle.ts, apps/keeper/src/rebalancer.ts, apps/keeper/src/optimizer.ts and docs/REBALANCING.md. If the code and this page disagree, the code wins.