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 flow
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 below for the full pattern.

rust
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:

rust
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:

rust
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:

rust
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

DirectionMessagePurpose
InboundPrivateKeyOptimizer sends a keypair to the bot
InboundAdjustConfigurationOptimizer pushes a new configuration struct
InboundPing / ShutdownLiveness and teardown
OutboundWallet(Pubkey)Bot reports its wallet address
OutboundPongResponse to Ping

Custom messages

You define these in message.rs. BouncerV1 uses:

DirectionMessagePurpose
InboundReturnWallet(Pubkey)Optimizer tells the bot where to sweep funds
OutboundLatency(tx_first_sig)Bot reports transaction latency

Start from BouncerV1

Clone the repo and copy BouncerV1 as your starting point:

bash
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, 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 via AdjustConfiguration.
  • 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, and CommitHook implementations.

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:

rust
use crate::brain::mybotv1::MyBotV1Hook;

// inside fn run():
let sampler = Rc::new(RefCell::new(MyBotV1Hook::default()));

Or use one of the built-in bots:

rust
use crate::brain::helloworldv1::HelloWorldV1Hook;
// or
use crate::brain::bouncerv1::BouncerV1Hook;

Then build:

bash
rustup target add wasm32-wasip2
cargo build --target wasm32-wasip2 --release

Compiled binary lands at:

text
target/wasm32-wasip2/release/catscope_rust_bot.wasm