Build · Bot Design
Build a bot in Rust.
From the sample template to transaction submission — how a Catscope bot is structured and how to extend it.
Catscope bots are designed to upload and run as WebAssembly modules inside Solana validators. Account data is delivered through shared memory, giving your bot the lowest possible latency — no RPC, no network hop, no gossip delay. On every slot tick, evaluate() is called with a fully up-to-date view of the accounts you care about, ready to act.
Note
The eval function
Every bot must define an evaluate() function in src/brain/<your-bot>/state.rs. It is called automatically at the end of every event your bot receives. This is where you read current state and decide what to do.
Event arrives
│
▼
handler updates self.state:
├── Commit → slot number, account balances, token balances
├── Account → low-latency balance update
├── Token → low-latency token balance update
├── Transaction → your transaction was confirmed
└── Stdin → message arrived from the optimizer
│
▼
evaluate() ← read state, act here
│
├── push to self.q_msg → message sent to the optimizer
└── transactionprocessor::send() → tx submitted to the validatorFrom evaluate() you can do two things:
Send a message to the optimizer
Report a price, a balance, a trade outcome. See custom messages below for the full pattern.
self.q_msg.push_back(MessageSend::Custom(
CustomMessageOutbound::PriceSignal { pool, price: self.state.last_price },
));Submit a transaction
Append instructions to the wallet, assemble, send:
let ix = system_instruction::transfer(&payer, &destination, amount);
self.wallet.append_ix(ix, 0);
if let Some((signature, tx_data)) = self.wallet.assemble() {
transactionprocessor::send(signature.as_ref(), tx_data).expect("send");
}Subscribing to accounts
Account subscriptions happen in on_load. You register which on-chain accounts you want the validator to stream to your bot. Updates arrive as events — no polling required.
The most common path is through the wallet. wallet.append_key registers a keypair and automatically subscribes to that account in the graph:
pub(crate) fn on_load(&mut self) {
let keypair = Keypair::new();
let g = self.o_graph.take().unwrap();
// registers the keypair and subscribes to its account in one step
let account_id = self.wallet.append_key(keypair, g).unwrap();
self.wallet.set_payer(account_id);
let pubkey = pubkey_from_account_id(&account_id).unwrap();
self.q_msg.push_back(MessageSend::Wallet(pubkey));
self.o_graph.replace(g);
}To subscribe to an arbitrary account (a DEX pool, a collateral account), use graph.subscribe directly:
let account_id = account_id_from_pubkey(&target_pubkey);
let _sub = graph.subscribe(account_id, 0, 0).unwrap();
// hold _sub — dropping it unsubscribesThe _sub value is a Subscription. As long as it is held, the validator streams updates for that account. Drop it to unsubscribe.
Message interface
Bots communicate with the optimizer over stdin/stdout. There are standard messages the framework handles automatically, and custom messages you define in message.rs.
Standard messages
| Direction | Message | Purpose |
|---|---|---|
| Inbound | PrivateKey | Optimizer sends a keypair to the bot |
| Inbound | AdjustConfiguration | Optimizer pushes a new configuration struct |
| Inbound | Ping / Shutdown | Liveness and teardown |
| Outbound | Wallet(Pubkey) | Bot reports its wallet address |
| Outbound | Pong | Response to Ping |
Custom messages
You define these in message.rs. BouncerV1 uses:
| Direction | Message | Purpose |
|---|---|---|
| Inbound | ReturnWallet(Pubkey) | Optimizer tells the bot where to sweep funds |
| Outbound | Latency(tx_first_sig) | Bot reports transaction latency |
Start from BouncerV1
Clone the repo and copy BouncerV1 as your starting point:
git clone https://github.com/noncepad/catscope-rust-bot
cp -r src/brain/bouncerv1 src/brain/mybotv1BouncerV1 is a fund-sweep bot — on load it generates a keypair, reports the pubkey to the optimizer, monitors its balance, and sweeps funds back to a return wallet on every commit. It is fully wired: subscriptions, message handling, transaction submission, and optimizer communication all work out of the box.
To build your own strategy, replace or extend three files in your copy:
configuration.rs— the struct the optimizer pushes down at runtime viaAdjustConfiguration.message.rs— custom inbound messages from the optimizer, and custom outbound signals your bot reports back.state.rs— your strategy logic:on_load,mid_on_account,evaluate, andCommitHookimplementations.
Receiving updates
Account data arrives through event handlers — mid_on_account, mid_on_token, and CommitHook::on_account — which run before evaluate() each cycle. Use them to store data into self.state so it's ready when evaluate() runs.
Building
Before building, select which bot to run in src/lib.rs. If you copied BouncerV1 into your own module (mybotv1), wire it up:
use crate::brain::mybotv1::MyBotV1Hook;
// inside fn run():
let sampler = Rc::new(RefCell::new(MyBotV1Hook::default()));Or use one of the built-in bots:
use crate::brain::helloworldv1::HelloWorldV1Hook;
// or
use crate::brain::bouncerv1::BouncerV1Hook;Then build:
rustup target add wasm32-wasip2
cargo build --target wasm32-wasip2 --releaseCompiled binary lands at:
target/wasm32-wasip2/release/catscope_rust_bot.wasm