How to Parse Percolator On-Chain Solana Perpetual Futures Data in CloddsBot

Percolator stores perpetual futures state in a single Solana account called the slab, which CloddsBot decodes by reading raw bytes and applying fixed-offset binary parsing to extract market configuration, risk parameters, and user positions.

Percolator is a Solana-based perpetual futures protocol that compresses the entire market state—including risk engine data and all user/LP accounts—into one on-chain account. The CloddsBot repository provides a complete TypeScript parser that transforms this binary blob into structured JavaScript objects. Understanding this parsing pipeline is essential for developers building automated trading strategies that react to on-chain perp market data.

Fetching the Raw Slab Account

The entry point for all Percolator data retrieval is the fetchSlab function in src/percolator/slab.ts. This utility uses a Solana Connection to retrieve the account info and return a Node.js Buffer containing the raw bytes.

// src/percolator/slab.ts
export async function fetchSlab(
  connection: Connection,
  slabPubkey: PublicKey,
): Promise<Buffer> {
  const info = await connection.getAccountInfo(slabPubkey);
  if (!info) {
    throw new Error(`Slab account not found: ${slabPubkey.toBase58()}`);
  }
  return Buffer.from(info.data);
}

The function validates the account exists before wrapping the data in a Buffer, ensuring downstream parsers receive consistent input.

Decoding the Binary Layout Constants

Percolator’s slab uses a fixed-size binary layout that mirrors the Rust structs used by the on-chain program. The parser defines a series of offset constants that map these memory positions:

  • MAGIC: Expected identifier "PERCOLAT" (hex 0x504552434f4c4154n)
  • HEADER_LEN: 72 bytes for the fixed header section
  • ENGINE_OFF: 392 bytes—start of the risk-engine region
  • ACCOUNT_SIZE: 240 bytes per user/LP account entry
  • BITMAP_WORDS: Number of 64-bit words tracking used account slots (64 words × 64 bits = 4096 potential slots)

These constants enable zero-copy style parsing where the decoder jumps directly to specific byte offsets without scanning the entire buffer.

Parsing 128-Bit Integers

Solana programs frequently use 128-bit signed and unsigned integers for token amounts and precise pricing. Since JavaScript Number types lose precision beyond 53 bits, the parser implements dedicated helpers:

function readI128LE(buf: Buffer, offset: number): bigint { … }
function readU128LE(buf: Buffer, offset: number): bigint { … }

These functions read two 64-bit little-endian values and combine them into a single bigint, handling sign-extension for the signed variant. All financial data in Percolator—including vault balances, position sizes, and funding indices—flows through these helpers to maintain numerical accuracy.

Extracting Market Configuration

Validating the Header

The parseHeader function validates the magic string and extracts metadata including version, bump seed, flags, admin authority, and resolution status.

export function parseHeader(data: Buffer): SlabHeader { … }

If the magic bytes do not match PERCOLAT, the parser throws immediately, preventing misinterpretation of unrelated account data.

Reading Risk Parameters

parseConfig extracts the MarketConfig struct starting at CONFIG_OFFSET. This includes the collateral mint address, vault token account, index price feed, funding parameters, price thresholds, and oracle authority addresses.

export function parseConfig(data: Buffer): MarketConfig { … }

Decoding Engine Risk Settings

Located at ENGINE_OFF + ENGINE_PARAMS_OFF, the RiskParams struct contains margin requirements, trading fees, liquidation fees, and position limits. The parseParams function returns these as a typed object for position validation logic.

export function parseParams(data: Buffer): RiskParams { … }

Capturing Engine State

parseEngine builds an EngineState object containing real-time market data: vault balance, insurance fund reserves, current Solana slot, funding index, total open interest, and crank cursor positions used by the on-chain execution engine.

export function parseEngine(data: Buffer): EngineState { … }

Processing User and LP Accounts

Finding Active Slots with Bitmap Parsing

Percolator supports up to 4096 concurrent user/LP accounts but uses a sparse storage model. The parseUsedIndices function walks a bitmap stored in the slab—64 words of 8 bytes each—to identify which account slots contain data.

export function parseUsedIndices(data: Buffer): number[] { … }

This returns an array of occupied indices, allowing the parser to skip empty 240-byte chunks and reduce processing overhead.

Parsing Individual Accounts

Given a slot index, parseAccount jumps to the corresponding offset (HEADER_LEN + bitmap size + index × ACCOUNT_SIZE) and decodes the 240-byte structure. It distinguishes between User accounts (traders with positions) and LP accounts (liquidity providers), mapping fields like owner public key, collateral balance, position size, entry price, and matcher program settings.

export function parseAccount(data: Buffer, idx: number): Account { … }

Aggregating All Accounts

parseAllAccounts combines the bitmap walker with the individual account parser to return a complete list of active positions:

export function parseAllAccounts(data: Buffer): { idx: number; account: Account }[] { … }

This produces an array of occupied slots with their decoded account data, ready for risk calculations or trade execution.

Integrating the Parsing Pipeline

In production, the PercolatorFeed component orchestrates the complete workflow. Located in src/percolator/feed.ts, it periodically calls fetchSlab, then executes the parsers in sequence to build a comprehensive market snapshot.

const slab = await fetchSlab(connection, slabPubkey);
const header = parseHeader(slab);
const config = parseConfig(slab);
const params = parseParams(slab);
const engine = parseEngine(slab);
const accounts = parseAllAccounts(slab);

The feed then emits typed events—price, orderbook, and positions—that downstream trading skills consume. This architecture separates the low-level binary parsing from high-level trading logic, allowing strategies to react to state changes without handling raw buffer offsets.

Complete Parsing Example

The following example demonstrates pulling a full market snapshot and filtering for a specific user’s positions:

import { Connection, PublicKey } from '@solana/web3.js';
import {
  fetchSlab,
  parseHeader,
  parseConfig,
  parseParams,
  parseEngine,
  parseAllAccounts,
} from './percolator/slab';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const slabPubkey = new PublicKey('PercolatorSlab111111111111111111111111111111');

// Fetch and decode
const slabData = await fetchSlab(connection, slabPubkey);
const header   = parseHeader(slabData);
const config   = parseConfig(slabData);
const risk     = parseParams(slabData);
const engine   = parseEngine(slabData);
const accounts = parseAllAccounts(slabData);

console.log(`Percolator v${header.version} | Vault: ${engine.vault.toString()}`);
console.log(`Open Interest: ${engine.totalOpenInterest.toString()}`);
console.log(`Active Accounts: ${accounts.length}`);

// Filter for specific user
const userKey = new PublicKey('User111111111111111111111111111111111111');
const userPositions = accounts.filter(a => a.account.owner.equals(userKey));
console.log(`User has ${userPositions.length} active positions`);

Summary

  • Percolator stores all perpetual futures data in a single Solana account (the slab) using a fixed binary layout.
  • CloddsBot parses this data through fetchSlab and a series of offset-based decoders defined in src/percolator/slab.ts.
  • 128-bit integer helpers ensure precise handling of token amounts and prices that exceed JavaScript’s safe integer range.
  • Bitmap tracking enables efficient iteration over up to 4096 potential user/LP accounts without scanning empty slots.
  • PercolatorFeed orchestrates periodic polling and emits structured events for downstream trading logic.

Frequently Asked Questions

How does the parser handle large integer values without losing precision?

The parser implements readI128LE and readU128LE functions that combine two 64-bit reads into a native JavaScript bigint. This approach preserves full 128-bit precision for vault balances, position sizes, and funding indices, avoiding the rounding errors that occur with standard JavaScript Number types.

What is the maximum number of accounts Percolator can store in one slab?

The slab supports 4,096 concurrent accounts tracked via a bitmap of 64-bit words (BITMAP_WORDS). The parseUsedIndices function walks this bitmap to identify occupied slots, and each active account consumes 240 bytes of storage space within the account region.

Why does Percolator use a single account instead of multiple PDA accounts?

Storing market configuration, risk parameters, engine state, and all user positions in one slab account reduces the Solana transaction complexity and account rent costs. This design requires sophisticated client-side parsing—implemented in src/percolator/slab.ts—but minimizes on-chain cross-program invocation overhead and state rental fees.

How does CloddsBot know if the slab data structure has changed?

The parseHeader function validates a magic string (PERCOLAT) and version field before processing. If the magic bytes do not match or the version is unsupported, the parser throws an error immediately. This mechanism ensures the bot fails fast when encountering incompatible program upgrades rather than silently misinterpreting binary offsets.

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 →