Skip to main content
Keepers are off-chain agents that monitor the Symmetry protocol and execute pending tasks in exchange for bounties. All keeper operations are permissionless — every SDK method in the rebalance flow (updateTokenPricesTx, flashSwapTx, mintTx, redeemTokensTx, claimBountyTx, etc.) can be called by any wallet. The keeper parameter is simply the signing wallet. Developers can build their own keeper bots using the individual SDK methods directly. The SDK also ships with KeeperMonitor and RebalanceHandler as reference implementations that handle the full lifecycle automatically — use them as-is, or as a starting point for custom keeper logic.

KeeperMonitor

KeeperMonitor is a reference keeper implementation included in the SDK. It continuously polls the protocol and automatically handles:
  • Executing configuration intents after activation
  • Cancelling expired intents
  • Processing rebalance intents (price updates, flash swaps, minting, redeeming, bounty claims)

Setup

import { KeeperMonitor } from "@symmetry-hq/sdk";
import { Connection, Keypair } from "@solana/web3.js";

const keypair = Keypair.fromSecretKey(/* your secret key */);
const wallet = {
  publicKey: keypair.publicKey,
  signTransaction: async (tx) => { tx.sign([keypair]); return tx; },
  signAllTransactions: async (txs) => { txs.forEach(tx => tx.sign([keypair])); return txs; },
  payer: keypair,
};

const connection = new Connection("https://api.mainnet-beta.solana.com");

const keeper = new KeeperMonitor({
  wallet,
  connection,
  network: "mainnet",
  jupiterApiKey: "<YOUR_JUPITER_API_KEY>",
  maxAllowedAccounts: 64,
  priorityFee: 50_000,
  simulateTransactions: false,
});

Running

Continuous loop:
while (true) {
  await keeper.update();
  await new Promise(resolve => setTimeout(resolve, 10_000));
}
Time-limited run:
await keeper.run(600); // run for 10 minutes (minimum 60 seconds)
The run() method calls update() every ~30 seconds, stops after the specified duration, waits 45 seconds for in-flight tasks, then clears internal state.

How It Works

Each update() call:
  1. Fetches all vaults and syncs them to an internal map.
  2. Fetches all configuration intents. For new intents, starts monitorIntent() in the background.
  3. Fetches all rebalance intents. For new actionable ones (not deposit_tokens or not_active), starts monitorRebalanceIntent() in the background.
  4. Removes closed intents from internal maps.

Intent Monitoring

For each configuration intent:
  1. Wait until activation_timestamp.
  2. Try to execute via executeVaultIntentTx (up to 2 attempts).
  3. If execution failed, wait until expiration.
  4. After expiration, try to cancel via cancelVaultIntentTx (up to 4 attempts).

Rebalance Intent Monitoring

For each rebalance intent:
  1. Skip if not_active or deposit_tokens (waiting for user action).
  2. If update_prices: call updateTokenPricesTx (up to 5 attempts).
  3. During auction windows:
    • Compute swap pairs via getSwapPairs.
    • Fetch Jupiter quotes for profitable pairs (refreshed every 60 seconds).
    • Execute flashSwapTx for each profitable pair.
    • Re-check every ~8 seconds.
  4. After auctions end:
    • Deposits: call mintTx (up to 3 attempts).
    • Withdrawals with remaining tokens: call redeemTokensTx (up to 3 attempts). Note: if the recipient’s ATA doesn’t exist for a token and the keeper is not the owner, that token is skipped during redemption. Ensure the withdrawer has ATAs for all expected tokens.
    • Then: call claimBountyTx (up to 3 attempts).

Requirements

RequirementDetails
SOL balanceKeeper wallet needs SOL for transaction fees
Jupiter API keyRequired for flash swap quotes on mainnet
RPC connectionReliable RPC with sufficient rate limits
UptimeKeepers should run continuously to not miss tasks

RebalanceHandler

RebalanceHandler handles a single rebalance intent from start to finish. Use this when you want to process a specific rebalance rather than monitoring all protocol activity.

One-Shot Run

import { RebalanceHandler } from "@symmetry-hq/sdk";
import { PublicKey } from "@solana/web3.js";

await RebalanceHandler.run({
  intentPubkey: new PublicKey("<REBALANCE_INTENT_PUBKEY>"),
  wallet,
  connection,
  network: "mainnet",
  jupiterApiKey: "<JUP_API_KEY>",
  maxAllowedAccounts: 64,
  priorityFee: 50_000,
  simulateTransactions: false,
});
This fetches the rebalance intent and its vault, creates a handler instance, runs the full lifecycle, and refreshes intent data periodically (every 15 seconds, up to 20 refreshes).

Manual Control

const handler = new RebalanceHandler({
  intent: uiRebalanceIntent,
  vault: vault,
  wallet,
  connection,
  network: "mainnet",
  jupiterApiKey: "<JUP_API_KEY>",
  maxAllowedAccounts: 64,
  priorityFee: 50_000,
  simulateTransactions: false,
});

Bounty System

Keepers earn bounties for completing tasks. Bounties are funded by the user or manager who creates the intent.

How Bounties Work

  1. When a deposit, withdrawal, rebalance, or configuration change is created, a bounty is deposited (typically in WSOL). This includes a bounty bond — a fixed amount (set in the protocol’s global config as bounty_bond_amount) that is locked alongside the bounty.
  2. The bounty has a schedule: starts at min_bounty and increases to max_bounty over time, incentivizing faster execution.
  3. Each task completion (price update, flash swap, mint, redeem) is recorded on-chain with the keeper’s pubkey and timestamp.
  4. After all tasks are done, claimBountyTx distributes bounties to all participating keepers proportionally based on the tasks they completed.
  5. Unused bounty and the bounty bond are returned to the depositor.
  6. The on-chain account rent is also returned.

Bounty Schedule

interface FormattedBountySchedule {
  min_bounty: number;           // raw amount in bounty token's smallest units
  max_bounty: number;           // raw amount in bounty token's smallest units
  min_bounty_until: number;
  max_bounty_after: number;
}
Between min_bounty_until and max_bounty_after, the bounty interpolates linearly.

Bounty Computation

The total WSOL locked when creating a deposit, withdrawal, or rebalance is computed automatically by the SDK. It includes:
ComponentDescription
Bounty bondFixed lock amount from global config (bounty_bond_amount). Returned after completion.
Task bountiesmax_bounty_per_task × number of tasks (auction stages, mint/redeem, claim).
Price update bountiesmax_bounty_per_task / bounty_per_price_update_task_divisor × number of tokens in the vault. More tokens = more price update instructions = higher total.
Users can override the min/max bounty per task via min_bounty_amount and max_bounty_amount parameters in buyVaultTx, sellVaultTx, and rebalanceVaultTx. Higher bounties incentivize keepers to process operations faster. The SDK computes the total automatically — callers do not need to calculate it manually.

Adding Bounty to a Vault

Vault bounty funds automation (keeper-initiated rebalances):
const tx: TxPayloadBatchSequence = await sdk.addBountyTx({
  keeper: wallet.publicKey.toBase58(),
  vault: "<VAULT_PUBKEY>",
  amount: 100_000_000,  // 0.1 SOL (raw: 0.1 * 10^9 = 100_000_000 lamports)
});

Jupiter Integration

The SDK includes utilities for interacting with Jupiter for flash swaps:
import { getJupTokenLedgerAndSwapInstructions } from "@symmetry-hq/sdk";

const result = await getJupTokenLedgerAndSwapInstructions({
  keeper: walletPublicKey,
  vaultMintIn: new PublicKey("<TOKEN_VAULT_RECEIVES>"),   // token deposited to vault
  vaultMintOut: new PublicKey("<TOKEN_VAULT_GIVES>"),     // token withdrawn from vault
  vaultAmountIn: 1_000_000,    // raw amount vault receives
  vaultAmountOut: 1_000_000,   // raw amount vault gives
  swapMode: "ioc",
  apiKey: "<JUPITER_API_KEY>",
  maxJupAccounts: 64,
});

// result contains:
// - tokenLedgerInstruction: TransactionInstruction
// - swapInstruction: TransactionInstruction
// - addressLookupTableAddresses: PublicKey[]
// - quoteResponse: { inAmount, outAmount, ... }

Swap Modes

ModeDescription
exact_inGuarantee exact input amount, variable output
exact_outGuarantee exact output amount, variable input
iocImmediate-or-cancel — best effort execution
The Jupiter API call uses reversed mint directions internally. The SDK handles this — you specify vaultMintIn (what the vault receives) and vaultMintOut (what the vault gives), and the SDK swaps them for the Jupiter query.