The keeper owns each market’s long and short positions on Meteora DLMM. Position operations go through CLP proxy instructions (Documentation Index
Fetch the complete documentation index at: https://continuum-ec12e897.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
clp_open_meteora_position, clp_remove_meteora_liquidity, etc.) so the CLP PDA - not the keeper wallet - owns the positions.
What the rebalancer does
Every 120s (DLMM_INTERVAL_SECS), for each market × side:
- Read on-chain
active_idand current position range from Meteora. - Compute NAV
target_binfrom Hermes price. - Compute vol-adaptive
bin_radiusfrom 15-min realized volatility. - Compute fee-weighted centroid from per-bin fee accumulators.
- If existing range still covers active and NAV target → skip (no rebuild).
- Else: remove + close + open with new range.
Volatility-adaptive bin_radius
The keeper maintains a 60-sample rolling price history per market (~15 min at 15s oracle cadence). Each rebalance computes annualized realized volatility from log returns and maps to a regime:
| Annualized vol | Regime | bin_radius | Price range (binStep=10) |
|---|---|---|---|
| < 10% | Calm | 10 | ±1.0% |
| < 25% | Normal | 20 | ±2.0% |
| < 50% | Elevated | 30 | ±3.0% |
| ≥ 50% | Stressed | 34 (max) | ±3.4% |
range = bin_radius × bin_step / 1000.
When history is too short (first ~5 ticks after boot), the keeper falls back to DLMM_BIN_RADIUS env var (default 20).
The thresholds are VIX-inspired heuristics. Per-asset percentile calibration is on the roadmap.
Fee-weighted position centering
For each rebalance, the keeper reads per-bin fee accumulators (fee_amount_x_per_token_stored, fee_amount_y_per_token_stored) across the existing position’s range. It computes a weighted centroid:
(active + NAV) / 2 midpoint. Bins that historically earned more fees bias the center toward them, capturing more future fee revenue for the same capital.
The centroid is clamped to the [active, NAV] span so coverage is preserved. Fresh pools with no fee history (total_weight == 0) fall back to the naive midpoint.
Skip-rebuild optimization
The rebalancer skips the full rebuild when:- Active bin hasn’t overflowed the position range, AND
- Existing range
[lower, upper]still covers the required span[min(active, target), max(active, target)]with 3-bin slack.
Position shape
CLP proxy uses Meteora’sadd_liquidity_by_strategy2 with two shapes:
amount_x, amount_y | strategy_type | Shape |
|---|---|---|
| Both > 0 | 4 (CurveBalanced) | Bell curve, ~60-70% of capital in central bins |
| Only X > 0 | 7 (CurveImBalanced) + singleSidedX flag | One-sided ask ladder |
active_id.
Configurable via DLMM_STRATEGY_TYPE.
Two-sided seed
On each fresh position, 50% of allocated capital is minted to L+S tokens (X side) and 50% stays as cUSDC (Y side). The open then passes both to Meteora, which places X above active and Y below. This means each side’s pool gets:- L pool: L tokens above active (asks), cUSDC below active (bids).
- S pool: S tokens above active (asks), cUSDC below active (bids).
Zombie position handling
Occasionally Meteora’sremove_liquidity_by_range2 leaves a position with phantom liq_share > 0 but all underlying bin arrays drained (liq_supply == 0). Meteora’s close_position2 then refuses to close (NonEmptyPosition error 6030).
The rebalancer detects zombies via is_position_zombie, skips the close step, and opens a fresh position. The zombie account’s ~0.05 SOL rent is sacrificed; all tokens have already been transferred out.
The rare alternative - keeper’s CLP-state pointer is lost while the Meteora account still exists - is handled by find_positions_in_pool which scans the pool for orphan CLP-owned positions. Operators can manually recover via recover-orphaned-position.ts.
Coverage gating
Rebalance requires either fresh Hermes price or on-chain oracle freshness. If both are stale, the keeper refuses to trigger a rebuild - the NAV target would come from stale data. Overflow-only rebalances (active bin blown past position edge) are allowed without NAV freshness, since they’re purely defensive.RPC-failure guard
position_exists returns Err on RPC rate-limit. The naive interpretation (unwrap_or(false)) caused a 61-position leak on an early devnet iteration - the keeper thought positions didn’t exist and opened duplicates. Current behavior treats Err as “alive” to prevent orphans.
Liquidity seeder
Every 600s (separate loop from rebalancer), the seeder allocates capital fromGlobalClp to per-market vaults:
- Edge-weighted allocation. Each market gets a weight blending its OI-cap share with its observed
(fee_rate + arb_rate) / (risk_weighted_q_deviation + drawdown)edge. The blend ramps from OI-weight (new markets) to edge-weight over the first $10k of paired mint volume. - Capital recall. When upcoming withdrawals (within
RECALL_LEAD_TIME_SECS) exceed the free global vault balance, worst-edge markets are unwound first. - Two-sided seed as described above.
Operator tunables
| Env var | Default | Effect |
|---|---|---|
DLMM_INTERVAL_SECS | 120 | Rebalance cadence |
DLMM_BIN_RADIUS | 20 | Fallback radius |
DLMM_REBALANCE_ENABLED | true | Master kill-switch |
DLMM_STRATEGY_TYPE | 4 | Position shape |
LIQUIDITY_SEED_ENABLED | true | Seeder loop |
LIQUIDITY_SEED_FRACTION_BPS | 6500 | Fraction of global vault per seed cycle |
DLMM_INTERVAL_SECS to reduce RPC load at the cost of slower rebalance reaction. Decrease DLMM_BIN_RADIUS (or the regime thresholds) to use less capital per market at the cost of narrower coverage.
What this means for users
- Pool depth tracks NAV. When NAV moves significantly, the keeper rebalances to keep liquidity around the new active price. You don’t have to time entries.
- Fee tier vs spread trade-off. Index ETFs (0.10%) have tighter peg bands but lower LP fees per swap. Volatile equities (0.25%) have wider bands but higher fees per swap.
- Zombie position rent loss is the protocol’s, not yours. Users never pay for keeper operational costs.
See also
Keeper overview
The rest of the keeper’s job: oracle, arb, seeder.
Arb paths
The four arb cycles in detail.

