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:
- Fetches all vaults and syncs them to an internal map.
- Fetches all configuration intents. For new intents, starts
monitorIntent() in the background.
- Fetches all rebalance intents. For new actionable ones (not
deposit_tokens or not_active), starts monitorRebalanceIntent() in the background.
- Removes closed intents from internal maps.
Intent Monitoring
For each configuration intent:
- Wait until
activation_timestamp.
- Try to execute via
executeVaultIntentTx (up to 2 attempts).
- If execution failed, wait until expiration.
- After expiration, try to cancel via
cancelVaultIntentTx (up to 4 attempts).
Rebalance Intent Monitoring
For each rebalance intent:
- Skip if
not_active or deposit_tokens (waiting for user action).
- If
update_prices: call updateTokenPricesTx (up to 5 attempts).
- 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.
- 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
| Requirement | Details |
|---|
| SOL balance | Keeper wallet needs SOL for transaction fees |
| Jupiter API key | Required for flash swap quotes on mainnet |
| RPC connection | Reliable RPC with sufficient rate limits |
| Uptime | Keepers 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
- 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.
- The bounty has a schedule: starts at
min_bounty and increases to max_bounty over time, incentivizing faster execution.
- Each task completion (price update, flash swap, mint, redeem) is recorded on-chain with the keeper’s pubkey and timestamp.
- After all tasks are done,
claimBountyTx distributes bounties to all participating keepers proportionally based on the tasks they completed.
- Unused bounty and the bounty bond are returned to the depositor.
- 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:
| Component | Description |
|---|
| Bounty bond | Fixed lock amount from global config (bounty_bond_amount). Returned after completion. |
| Task bounties | max_bounty_per_task × number of tasks (auction stages, mint/redeem, claim). |
| Price update bounties | max_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
| Mode | Description |
|---|
exact_in | Guarantee exact input amount, variable output |
exact_out | Guarantee exact output amount, variable input |
ioc | Immediate-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.