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:

  1. Reads the current oracle price from oracleAddress
  2. Invokes the matcher program (if configured) to settle pending LP orders
  3. 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:

  1. Configuration Loading – The bot loads PercolatorConfig from src/utils/config.ts, validating that slabAddress is present and optionally populating matcherProgram, matcherContext, and keeperEnabled.

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

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

  4. Trade Execution – When a skill invokes execution.marketBuy(), the execution service constructs a transaction referencing the current matcherProgram and matcherContext from 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 matcherProgram and matcherContext in PercolatorConfig, 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 PercolatorConfig interface, 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →