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:
- Read rates
Fetch every venue's APY, TVL and utilisation from the venue APIs or DefiLlama, cached for 5 minutes. See rate sources.
- 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.
- Read the vault
Read idle, total assets and every strategy position from the chain.
- Smooth, solve and plan
Update the smoothed APYs, solve target weights and decide which moves are worth making this cycle.
- 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.
- 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:
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.
| Parameter | Bedrock | Rapids |
|---|---|---|
| λ, risk aversion (APY points per risk score²) | 0.08 | 0.025 |
| γ, concentration penalty | 6 | 4 |
| Idle buffer | 5% | 5% |
| Per-chain cap / cross-chain cap | 50% / 50% | 50% / 50% |
| Minimum venues with a floor weight | 3 at 5% | 3 at 5% |
| Utilisation penalty | Linear above 90%, 2 points at 100% | Same |
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:
- 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.
- 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.
- 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.
- Venue-to-venue moves. Only when every condition below holds. Each move closes at most half of a venue's gap.
| Condition | Default |
|---|---|
| One end of the move is outside the drift band | Larger of 1.5% of the vault, 15% of the target and the minimum move |
| The move is at least the minimum size | Larger of 100 USDC and 0.5% of the vault |
| No reallocation in the vault recently | 60 minutes |
| Neither venue moved recently | 6 hours |
| The cost check passes | Gain at least 2× cost and 1 USDC more than it |
| Turnover budget left | 10% of the vault per cycle, 20% per rolling 24 hours |
| A venue above the 35% concentration limit | Trimmed 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:
| Leg | Fixed | Proportional |
|---|---|---|
| Solana venue | 0.01 USDC | 1 bp |
| Cross-chain venue (CCTP) | 5 USDC | 2 bp |
| Idle | Free | Free |
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:
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.
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.
| Field | Meaning |
|---|---|
kind | hold, rebalance, or derisk (at least one sentinel exit) |
summary | One line for people, such as the move made or the main reason for holding |
moves | Executed flows as from → to → amount, paired across venues; leftovers go to or come from idle |
expectedApyBefore / After | Net APY of the vault on smoothed rates, before and after the moves |
sentinel | The sentinel's summary verdict, score and note for the tier |
signatures | Solana transaction signatures of the executed moves |
rationale.legs | Each planned leg: derisk, breach, concentration, yield or deploy, with spread, gain, cost and a note |
rationale.held | Why other candidate moves were not made, most relevant first (up to 5) |
rationale.frozen | Venues that could not receive funds this cycle |
rationale.turnover24hUsd | Venue-to-venue volume in the last 24 hours, and its cap |
rationale.smoothedApy / spotApy | The rate the plan used and the latest sample, per venue |
/decisionsHow 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 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.
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.
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.