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.
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 validator
From evaluate() you can do two things:
Send a message to the optimizer — report a price, a balance, a trade outcome. See Custom Messages for how to define and send them.
self.q_msg.push_back(MessageSend::Custom(
CustomMessageOutbound::PriceSignal { pool, price: self.state.last_price },
));
Submit a transaction — append instructions to the wallet, assemble, and 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, etc.), 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 unsubscribes
The _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/mybotv1
BouncerV1 is a fund sweep bot — on load it generates a keypair, reports the pubkey to the optimizer, then 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 inside your copy:
configuration.rs— the struct the optimizer pushes down at runtime viaAdjustConfigurationmessage.rs— custom inbound messages from the optimizer, and custom outbound signals your bot reports backstate.rs— your strategy logic:on_load,mid_on_account,evaluate, andCommitHookimplementations
For strategy patterns and examples of what to put in these files, see Use Cases.
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 is ready when evaluate() runs.
Building
Before building, select which bot to run in src/lib.rs. If you copied BouncerV1 into your own module (e.g. mybotv1), import your hook and wire it up here:
use crate::brain::mybotv1::MyBotV1Hook;
// inside fn run():
let sampler = Rc::new(RefCell::new(MyBotV1Hook::default()));
Or to use one of the built-in bots, swap the import and instantiation:
use crate::brain::helloworldv1::HelloWorldV1Hook;
// or
use crate::brain::bouncerv1::BouncerV1Hook;
Then build:
rustup target add wasm32-wasip2
cargo build --target wasm32-wasip2 --release
The compiled binary will be at:
target/wasm32-wasip2/release/catscope_rust_bot.wasm