How Percolator Coordinates Pluggable Matchers, Keeper Cranking, and Real-Time Slab Polling
Percolator synchronizes pluggable matchers, keeper cranking, and real-time slab polling through a shared PercolatorConfig that binds the keeper’s periodic crank transactions to the feed’s polling logic, ensuring the on-chain order book stays fresh while allowing hot-swappable matching engines.
The Percolator module in CloddsBot provides a Solana-native perpetual futures infrastructure that separates market data ingestion from order execution. By decoupling the keeper (state advancement), feed (real-time polling), and matcher (pluggable program IDs), the system achieves low-latency coordination without tight coupling between components.
Architecture Overview
The Percolator subsystem consists of three loosely-coupled services defined in src/percolator/index.ts. The createPercolatorService factory instantiates:
- A keeper that periodically sends crank transactions to advance the on-chain slab
- A feed that polls the slab account via RPC and emits parsed market state
- An execution service that routes trades through configurable matcher programs
All three components reference the same PercolatorConfig interface declared in src/percolator/types.ts, ensuring they operate on identical slab addresses, oracle feeds, and optional matcher program IDs.
The Three Core Components
Pluggable Matchers
Pluggable matchers allow operators to swap the on-chain matching logic without restarting the bot. The PercolatorConfig accepts optional matcherProgram and matcherContext fields that identify the Solana program ID and context account for the active matcher.
When createPercolatorExecution builds a transaction in src/percolator/execution.ts, it includes these accounts as writable references in the CPI instruction. If the matcher fields are omitted, the system falls back to the default matching engine.
The feed service also inspects these fields to detect passive matcher LP status. When matcherProgram is defined, the feed treats the matcher as an active liquidity provider, influencing how it calculates available liquidity from the slab parser in src/percolator/slab.ts.
Keeper Cranking
The keeper is an optional background task created by createPercolatorKeeper in src/percolator/keeper.ts. When keeperEnabled is set to true in the configuration, the keeper starts a timer that fires at intervalMs frequency (default 5000ms).
Each tick executes a crank transaction defined in src/percolator/tx.ts. The crank:
- Reads the current oracle price from
oracleAddress - Invokes the matcher program (if configured) to settle pending LP orders
- Advances the slab state stored at
slabAddress
If the slab or oracle address is missing, the keeper logs a warning and skips the iteration, preventing failed transactions.
Real-Time Slab Polling
The feed service, instantiated via createPercolatorFeed in src/percolator/feed.ts, maintains a continuous WebSocket or HTTP polling loop against the slabAddress. Each poll fetches the raw account buffer and passes it to the binary parser in src/percolator/slab.ts.
The parser extracts bestBid, bestAsk, lastPrice, openInterest, and user-specific positions via getPositions. When the parsed PercolatorMarketState differs from the previous iteration, the feed emits a 'state' event. Because the feed and keeper target the same slab account, the keeper’s cranking guarantees the feed never propagates stale order-book data.
Coordination Flow
The synchronization between components follows four distinct phases:
-
Configuration Loading – The bot loads
PercolatorConfigfromsrc/utils/config.ts, validating thatslabAddressis present and optionally populatingmatcherProgram,matcherContext, andkeeperEnabled. -
Keeper Initialization – If enabled, the keeper begins its interval loop in
src/percolator/keeper.ts, committing crank transactions that update the on-chain slab. Each successful crank advances the market state and refreshes the oracle price. -
Feed Polling – Concurrently, the feed polls the identical
slabAddress. Since Solana confirms transactions within 400-600ms, the feed typically observes the keeper’s updates within one or two polling cycles. -
Trade Execution – When a skill invokes
execution.marketBuy(), the execution service constructs a transaction referencing the currentmatcherProgramandmatcherContextfrom the config. The transaction targets the same slab the feed is polling, ensuring the trade executes against the latest liquidity.
Implementation Examples
Initializing the Percolator Service
import { createPercolatorService } from './percolator/index.js';
import type { PercolatorConfig } from './percolator/types.js';
const percolatorConfig: PercolatorConfig = {
slabAddress: process.env.PERCOLATOR_SLAB,
oracleAddress: process.env.PERCOLATOR_ORACLE,
matcherProgram: process.env.PERCOLATOR_MATCHER_PROGRAM,
matcherContext: process.env.PERCOLATOR_MATCHER_CONTEXT,
keeperEnabled: true,
keeperIntervalMs: 5_000,
};
const { feed, execution, keeper } = createPercolatorService(percolatorConfig);
Listening for Market State Updates
feed.on('state', (state) => {
console.log('🧭 New Percolator market state:', state);
});
Manual Keeper Cranking
if (keeper) {
const { crank } = await import('./percolator/tx.js');
await crank(percolatorConfig);
}
Executing Trades Through Pluggable Matchers
await execution.marketBuy({ size: 1_000_000n });
Summary
- Pluggable matchers are configured via
matcherProgramandmatcherContextinPercolatorConfig, allowing hot-swapping of matching logic without service restarts. - Keeper cranking runs as an optional background interval task in
src/percolator/keeper.ts, periodically advancing the on-chain slab state and refreshing oracle prices. - Real-time slab polling occurs in
src/percolator/feed.ts, parsing binary slab data from the same address the keeper updates, ensuring fresh market data. - All three components coordinate through the shared
PercolatorConfiginterface, with the keeper acting as the state producer and the feed acting as the state consumer.
Frequently Asked Questions
How does the keeper prevent failed transactions when the slab is unavailable?
The createPercolatorKeeper function in src/percolator/keeper.ts validates that both slabAddress and oracleAddress exist in the config before executing the crank. If either address is undefined, the keeper logs a warning and skips the iteration, avoiding RPC errors or failed on-chain transactions.
Can I run the feed without the keeper enabled?
Yes. The feed operates independently in src/percolator/feed.ts and polls the slabAddress regardless of the keeper’s status. However, without the keeper running, the slab state only updates when external transactions modify the order book, potentially resulting in stale data during low-activity periods.
What happens if I change the matcher program ID while the bot is running?
The execution service reads matcherProgram and matcherContext from the config at transaction build time. If you update these environment variables and restart the bot, subsequent trades will route through the new matcher. The keeper also references these fields when cranking, ensuring the new matcher settles liquidity correctly after the restart.
How frequently does the slab polling update market state?
The feed polls the slab account continuously via WebSocket or HTTP, while the keeper advances the state at keeperIntervalMs intervals (default 5000ms). In practice, the feed observes new state within milliseconds of the keeper’s transaction confirmation, as Solana slots finalize in approximately 400-600ms.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →