noncepad/catscope-rust-bot

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

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