Skip to main content
Rebalancing is the mechanism by which vaults process deposits, withdrawals, and periodic rebalances to maintain target weights. All three operations use the same rebalance intent flow.

Rebalance Types

TypeEnumDescription
Deposit0User deposits tokens → receives vault tokens
Withdraw1User burns vault tokens → receives underlying tokens
Vault2Keeper-initiated rebalance to restore target weights
VaultCustom3Custom vault rebalance

Lifecycle

Each rebalance type follows a similar on-chain flow, but with type-specific stages: Deposit flow:
[Create] → [Deposit Tokens] → [Lock Deposits] → [Update Prices]
  → [Auction 1] → [Auction 2] → [Auction 3]
  → [Mint] → [Claim Bounty] → [Close Account]
Withdraw flow:
[Create] → [Update Prices]
  → [Auction 1] → [Auction 2] → [Auction 3]
  → [Redeem] → [Claim Bounty] → [Close Account]
Vault / VaultCustom flow:
[Create] → [Update Prices]
  → [Auction 1] → [Auction 2] → [Auction 3]
  → [Claim Bounty] → [Close Account]
ActionEnumDescription
not_active0Intent not yet initialized
deposit_tokens1User is depositing tokens (deposits only)
update_prices3Oracle prices need to be refreshed
auction4Auction phase — keepers execute flash swaps
Mint, redeem, and claim bounty are terminal task stages — they do not appear as separate current_action enum values but are handled after the auction phase completes.

Obtaining a Rebalance Intent Key

Many SDK methods require a rebalance_intent pubkey. You can obtain it in two ways:
  1. Deterministic PDA — The rebalance intent address is derived from [REBALANCE_INTENT_SEED, vault_key, owner_key]. Use this when you know both the vault and the owner.
  2. Fetch from on-chain — Query active intents:
const ownerIntents = await sdk.fetchOwnerRebalanceIntents("<OWNER_PUBKEY>");
const vaultIntents = await sdk.fetchVaultRebalanceIntents("<VAULT_PUBKEY>");

Deposits

Each user can have only one active deposit or withdrawal per vault at a time. Starting a new deposit while a previous one is still processing will fail because the rebalance intent account (a PDA derived from the vault and user addresses) already exists.

Step 1: Create Deposit Intent and Deposit Tokens

// Amounts are raw (smallest units): SOL = 9 decimals, USDC = 6 decimals
const tx: TxPayloadBatchSequence = await sdk.buyVaultTx({
  buyer: wallet.publicKey.toBase58(),
  vault_mint: "<VAULT_TOKEN_MINT>",
  contributions: [
    { mint: "So11111111111111111111111111111111111111112", amount: 1_000_000_000 },   // 1 SOL
    { mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", amount: 100_000_000 }, // 100 USDC
  ],
  rebalance_slippage_bps: 100,
  per_trade_rebalance_slippage_bps: 100,
});

await sdk.signAndSendTxPayloadBatchSequence({
  txPayloadBatchSequence: tx,
  wallet,
});
This deposits the contributed tokens and starts the deposit process. The contributions array specifies which tokens to deposit and how much. All amounts are raw values in the token’s smallest unit (e.g., lamports for SOL, 10^6 units for USDC). Users can contribute any tokens — they do NOT need to match the vault’s composition. The rebalance auction will swap tokens as needed. The vault_mint parameter is the vault’s SPL token mint address (not the vault account address).

Step 2 (Optional): Deposit More Tokens

Before locking, users can add more contributions:
const tx = await sdk.depositTokensTx({
  buyer: wallet.publicKey.toBase58(),
  contributions: [{ mint: "<MINT>", amount: 500_000_000 }],
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});

Step 3: Lock Deposits

Locking freezes contributions and starts the rebalance process:
const tx = await sdk.lockDepositsTx({
  buyer: wallet.publicKey.toBase58(),
  vault_mint: "<VAULT_TOKEN_MINT>",
});
After locking, the intent moves to update_prices. From here, keepers typically take over — but users can execute any of these steps themselves using the same SDK methods.

Steps 4–7: Processing

These steps are typically handled by keepers automatically, but any user can call these methods directly:
  1. Update PricesupdateTokenPricesTx refreshes oracle prices.
  2. Auctions — Three auction windows where flash swaps are executed via flashSwapTx.
  3. MintmintTx mints vault tokens to the depositor.
  4. Claim BountyclaimBountyTx distributes rewards and closes the account.
Any wallet may execute these stage methods directly; keeper is an actor role, not a privilege role.
After vault tokens are minted, any deposited tokens not consumed during minting are returned to the depositor. This happens automatically: the rebalance intent transitions to a withdrawal phase. Since the depositor’s contributed tokens are marked as “keep tokens,” the return skips the auction entirely and proceeds directly to redemption.

Withdrawals

const tx: TxPayloadBatchSequence = await sdk.sellVaultTx({
  seller: wallet.publicKey.toBase58(),
  vault_mint: "<VAULT_TOKEN_MINT>",
  withdraw_amount: 1_000_000,     // raw vault token amount to burn
  keep_tokens: [],
  rebalance_slippage_bps: 100,
  per_trade_rebalance_slippage_bps: 100,
});
When the vault has performance fees configured (any of host/creator/managers performance fee > 0), the withdrawal follows a deferred path:
  1. The vault token burn amount and fee calculations are recorded but token amounts are not immediately subtracted from the vault.
  2. The vault’s active_withdraws counter is incremented.
  3. During the updateTokenPricesTx step, performance fees are calculated, token amounts are subtracted from the vault, and supply_outstanding is adjusted.
This means withdrawals in vaults with performance fees cannot proceed concurrently with pending management intents (active_managements must be 0).

keep_tokens Behavior

The keep_tokens parameter controls which tokens the user receives directly without going through auctions:
  • Empty ([]) — Full rebalance flow. Keepers update prices, run 3 auction windows to swap tokens toward a single output, then redeem. This is the slowest path.
  • Partial ([mint1, mint2]) — The specified tokens are sent directly to the user. The remaining tokens still go through price updates and auctions.
  • All vault mintsFast withdrawal. When every token in the vault’s composition (including active: false slots) is passed, the protocol skips price updates and auctions entirely. The intent goes directly to the redeem stage. The user receives their proportional share of each underlying token. This still requires two transactions: sellVaultTx to create the intent, then redeemTokensTx to transfer the tokens. The user can call redeemTokensTx themselves — no need to wait for a keeper.

Fast Withdrawal

When every token in the vault’s composition is passed in keep_tokens, the protocol skips price updates and auctions. The intent goes directly to the redeem stage. This is two transactions: sellVaultTx then redeemTokensTx.
const vault: Vault = await sdk.fetchVault("<VAULT_PUBKEY>");
const allMints: string[] = vault.formatted!.composition.map(asset => asset.mint);

const sellTx: TxPayloadBatchSequence = await sdk.sellVaultTx({
  seller: wallet.publicKey.toBase58(),
  vault_mint: vault.formatted!.mint,
  withdraw_amount: 1_000_000,
  keep_tokens: allMints,
  rebalance_slippage_bps: 100,
});
await sdk.signAndSendTxPayloadBatchSequence({ txPayloadBatchSequence: sellTx, wallet });

const redeemTx: TxPayloadBatchSequence = await sdk.redeemTokensTx({
  keeper: wallet.publicKey.toBase58(),
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
await sdk.signAndSendTxPayloadBatchSequence({ txPayloadBatchSequence: redeemTx, wallet });
The keeper parameter in redeemTokensTx is just the signing wallet — any user can call this directly.

Standard Withdrawal Flow

After creating the withdrawal intent, keepers typically process it — but users can also execute each step themselves:
  1. Update Prices (updateTokenPricesTx)
  2. Auctions (flashSwapTx)
  3. Redeem Tokens (redeemTokensTx)
  4. Claim Bounty (claimBountyTx)
Any wallet may execute these stage methods directly; keeper is an actor role, not a privilege role.

Vault Rebalance (Keeper-Initiated)

Keepers can trigger a rebalance to bring the vault back to its target weights. A rebalance is only allowed when all of the following conditions are met:
  1. Automation is enabledautomation_settings.enabled must be true (configured via editAutomationTx).
  2. No active rebalance — the vault must not already have a deposit, withdrawal, or rebalance in progress.
  3. Bounty balance — the vault must have bounty funds to pay the keeper (added via addBountyTx).
  4. Within the automation window — the current time must fall within the vault’s schedule automation window (automation_start to automation_end within the cycle). See Schedule & Cycles for details.
  5. Cooldown elapsed — enough time must have passed since the last automated rebalance (rebalance_activation_cooldown seconds).
  6. Threshold exceeded — at least one non-bounty token must have drifted beyond both the absolute and relative deviation thresholds. The bounty token (typically WSOL) is excluded from this check.
    • rebalance_activation_threshold_abs_bps — minimum deviation as a percentage of total vault value (e.g., 500 = 5%). Computed as |actual_value - target_value| / vault_tvl.
    • rebalance_activation_threshold_rel_bps — minimum deviation relative to the token’s own target (e.g., 1000 = 10%). Computed as |actual_value - target_value| / max(actual_value, target_value).
The isRebalanceRequired() utility checks all of these conditions:
import { isRebalanceRequired } from "@symmetry-hq/sdk";

const needed: boolean = await isRebalanceRequired(vault, connection);

if (needed) {
  const tx: TxPayloadBatchSequence = await sdk.rebalanceVaultTx({
    keeper: wallet.publicKey.toBase58(),
    vault_mint: "<VAULT_TOKEN_MINT>",
    rebalance_slippage_bps: 100,            // 1% overall slippage tolerance
    per_trade_rebalance_slippage_bps: 100,  // 1% per-trade slippage
  });

  await sdk.signAndSendTxPayloadBatchSequence({
    txPayloadBatchSequence: tx,
    wallet,
  });
}
These thresholds are configured via editAutomationTx:
const tx = await sdk.editAutomationTx(
  { vault: "<VAULT_PUBKEY>", manager: wallet.publicKey.toBase58() },
  {
    enabled: true,
    rebalance_slippage_threshold_bps: 100,              // 1% max slippage during auctions
    per_trade_rebalance_slippage_threshold_bps: 100,     // 1% per-trade slippage
    rebalance_activation_threshold_abs_bps: 500,         // 5% of TVL absolute deviation triggers rebalance
    rebalance_activation_threshold_rel_bps: 1000,        // 10% relative deviation triggers rebalance
    rebalance_activation_cooldown: 3600,                 // minimum 1 hour between rebalances
    modification_delay: 86400,                           // 24h delay for future automation changes
  }
);

Auction System

After price updates, the rebalance enters three sequential auction stages. Each stage has a fixed duration configured in the protocol’s global config (rebalance_auction_1_timeframe, rebalance_auction_2_timeframe, rebalance_auction_3_timeframe). During each stage, keepers can execute flash swaps to move the vault toward its target weights.

Auction Pricing

The vault uses oracle confidence bands to create a Dutch-auction-style pricing curve that crosses through fair value, incentivizing timely execution:
  • At auction start: The vault sells tokens at price + confidence (expensive for keepers) and buys tokens at price - confidence (cheap for keepers). This is the widest unfavorable spread.
  • Over time: The spread narrows linearly toward mid-price. The price delta follows: conf × 2 × time_elapsed / auction_duration.
  • At the midpoint: Both buy and sell prices converge to the oracle mid-price. The spread is zero.
  • After the midpoint: The prices cross through mid-price and the vault begins offering better-than-market rates — selling below mid-price and buying above mid-price. This creates increasing profit opportunity for keepers.
  • At auction end: Sell price reaches price - confidence and buy price reaches price + confidence — the maximum favorable spread for keepers.
This crossing mechanism means keepers face a trade-off: executing early gets worse pricing but less competition; waiting past the midpoint gets better pricing but risks another keeper taking the swap first. Each new auction stage resets this convergence.

After Auctions End

Once all three auction stages complete:
  • Deposits: Vault tokens are minted to the depositor via mintTx.
  • Withdrawals: Underlying tokens are sent to the withdrawer via redeemTokensTx.
  • Vault rebalances: The rebalance completes directly.
After minting or redeeming is complete, the bounty is claimed via claimBountyTx, which distributes bounty rewards to all keepers who completed tasks during the rebalance (proportional to the tasks they performed), awards a task bounty to the caller, returns unused bounty and the bounty bond to the depositor, and closes the rebalance intent account.

Flash Swaps

A flash swap is an atomic operation within a single transaction:
  1. Flash Withdraw — Tokens are withdrawn from the vault to the keeper.
  2. Jupiter Swap — Keeper swaps the tokens via Jupiter (or any other DEX).
  3. Flash Deposit — Keeper deposits the swapped tokens back into the vault.
The vault gives the keeper mint_out tokens and expects mint_in tokens back. The keeper profits from any spread between the vault’s auction price and the market price.
import { getSwapPairs, getJupTokenLedgerAndSwapInstructions } from "@symmetry-hq/sdk";

const pairs: SwapPair[] = getSwapPairs(rebalanceIntent.chain_data, vault);

for (const pair of pairs) {
  const jupResult = await getJupTokenLedgerAndSwapInstructions({
    keeper: wallet.publicKey,
    vaultMintIn: new PublicKey(pair.inMint),
    vaultMintOut: new PublicKey(pair.outMint),
    vaultAmountIn: pair.inAmount,
    vaultAmountOut: pair.outAmount,
    swapMode: "ioc",
    apiKey: "<JUP_API_KEY>",
    maxJupAccounts: 64,
  });

  const tx: TxPayloadBatchSequence = await sdk.flashSwapTx({
    keeper: wallet.publicKey.toBase58(),
    vault: "<VAULT_PUBKEY>",
    rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
    mint_in: pair.inMint,       // token the vault receives
    mint_out: pair.outMint,     // token the vault gives
    amount_in: pair.inAmount,   // raw amount deposited to vault
    amount_out: pair.outAmount, // raw amount withdrawn from vault
    mode: 2,                    // IOC (immediate-or-cancel)
    jup_token_ledger_ix: jupResult.tokenLedgerInstruction,
    jup_swap_ix: jupResult.swapInstruction,
    jup_address_lookup_table_addresses: jupResult.addressLookupTableAddresses,
  });

  await sdk.signAndSendTxPayloadBatchSequence({
    txPayloadBatchSequence: tx,
    wallet,
  });
}

Flash Swap Parameters

ParameterDescription
keeperKeeper’s public key
vaultVault account public key
rebalance_intentRebalance intent public key (for rebalance swaps)
intentIntent public key (for direct swap intents)
mint_inToken the vault receives (keeper deposits this)
mint_outToken the vault gives (keeper receives this)
amount_inAmount of mint_in to deposit
amount_outAmount of mint_out to withdraw
mode0 = exact_in, 1 = exact_out, 2 = IOC (immediate-or-cancel)
jup_token_ledger_ixOptional: Jupiter token ledger instruction
jup_swap_ixOptional: Jupiter swap instruction
jup_address_lookup_table_addressesOptional: Jupiter ALT addresses

Swap Pairs

getSwapPairs(rebalanceIntent, vault) computes all valid swap pairs from the current auction state:
{
  inMint: string;
  outMint: string;
  inAmount: number;
  outAmount: number;
  value: number;
}
Pairs with value < 0.005 are typically skipped as not worth the transaction cost.

Minting Vault Tokens

After auction windows close for a deposit rebalance:
const tx: TxPayloadBatchSequence = await sdk.mintTx({
  keeper: wallet.publicKey.toBase58(),  // signer — any wallet can call this
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
This mints vault tokens to the depositor proportional to the value of their contribution minus fees.

Redeeming Tokens

After auction windows close for a withdrawal rebalance:
const tx: TxPayloadBatchSequence = await sdk.redeemTokensTx({
  keeper: wallet.publicKey.toBase58(),  // signer — any wallet can call this
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
This sends the underlying tokens to the withdrawer.
The withdrawer must have existing Associated Token Accounts (ATAs) for all tokens being redeemed. If an ATA doesn’t exist for a token and the transaction is submitted by a keeper (not the withdrawer themselves), that token will be skipped during redemption. Create ATAs for all expected tokens before the redemption step.

Claiming Bounty

After minting or redeeming is complete:
const tx: TxPayloadBatchSequence = await sdk.claimBountyTx({
  keeper: wallet.publicKey.toBase58(),  // signer — any wallet can call this
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
This distributes earned bounties to all keepers who completed tasks during the rebalance, awards a task bounty to the caller, returns unused bounty to the depositor, and closes the rebalance intent account.

Price Updates During Rebalance

Keepers must update oracle prices before the auction can begin:
const tx = await sdk.updateTokenPricesTx({
  keeper: wallet.publicKey.toBase58(),
  vault: "<VAULT_PUBKEY>",
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
This handles all the complexity of Pyth VAA creation, verification, and feed updates automatically. The batch layout:
  1. Create and initialize Pyth VAAs
  2. Write and verify VAAs
  3. Update individual price feeds
  4. Update token prices in the vault + close VAA accounts

Standalone Pyth Price Update

To update Pyth prices without a rebalance context:
const tx: TxPayloadBatchSequence = await sdk.updatePythPriceFeedsTx({
  keeper: wallet.publicKey.toBase58(),
  accounts: ["<PYTH_PRICE_ACCOUNT_1>", "<PYTH_PRICE_ACCOUNT_2>"],
});

Cancelling a Rebalance

const tx = await sdk.cancelRebalanceIntentTx({
  keeper: wallet.publicKey.toBase58(),
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});

Fetching Rebalance Intents

const ri = await sdk.fetchRebalanceIntent("<PUBKEY>");

const riMap = await sdk.fetchMultipleRebalanceIntents(["<PUBKEY_1>", "<PUBKEY_2>"]);

const all = await sdk.fetchAllRebalanceIntents();
const byOwner = await sdk.fetchOwnerRebalanceIntents("<OWNER_PUBKEY>");
const byVault = await sdk.fetchVaultRebalanceIntents("<VAULT_PUBKEY>");

Checking If Rebalance Is Needed

import { isRebalanceRequired } from "@symmetry-hq/sdk";

const needed = await isRebalanceRequired(vault, connection);
// Returns true when all conditions are met: automation enabled, no active rebalance,
// bounty available, within schedule window, cooldown elapsed, and threshold exceeded.

Concurrent Operations

Deposits and withdrawals can proceed while a vault rebalance is in progress. When a deposit mints tokens or a withdrawal redeems tokens during an active vault rebalance, the vault rebalance intent’s token amounts and target amounts are automatically updated to reflect the changed vault composition. This prevents stale data in the rebalance auction. However, vault rebalances cannot run concurrently with each other (only one vault rebalance can exist at a time), and they require active_managements == 0 (no pending configuration changes).

Rebalance Intent Data Structure

interface UIRebalanceIntent {
  rebalance_type: "deposit" | "withdraw" | "vault" | "vault_custom";
  deposit_data: DepositData | null;
  price_updates_data: PriceUpdatesData | null;
  auction_data: AuctionData | null;
  mint_data: MintData | null;
  redeem_data: RedeemData | null;
  claim_bounty_data: ClaimBountyData | null;
  formatted_data: FormattedRebalanceIntent;
  chain_data: RebalanceIntent;
}