Skip to content

Processor contracts

These processors ship in the standard binary. Downstream projects can assemble a custom binary and register independent native processors through the public factory/registry API; see Build a custom processor.

blobs-money 1.6.0

Block-local Dencun-and-later blob economics and type-3 transaction output. It accepts the filtered Xatu projection or complete normalized execution frames. It declares the Header, Body, Senders, BlobFields, BlockSize and ReceiptOutcome field groups, filtered to type-3 transactions. All wei quantities are lossless integers.

history_mode = "on_demand" disables only automatic historical processor coverage. It does not disable canonical live processing, normal JSON-RPC, or WebSocket head subscriptions, and it still allows [processors.live_gap_fill], which fills short ranges the live lane skips (see Live gap fill).

The processor emits one atomic block change containing the block and all of its blob transactions. Materialized output stores only the block entity, transaction entities, and transaction-by-block index; snapshot queries reconstruct that same bundle without retaining a second permanent copy. The processor retains the exact post-Fusaka BPO activation schedule and keeps consensus-block size distinct from the exact execution-block RLP size.

Each block uses the parameters of the fork active at its timestamp. The blob base fee and every blob burn use the protocol fee, EIP-4844’s fake_exponential(1, excess_blob_gas, update_fraction), under every fork. From Fusaka on, the EIP-7918 reserve price (execution base fee × 2^13 / 2^17) is reported separately as reserveFeeWei and is not applied to the fee. Version 1.5 fixes the fee and burn that 1.4 overstated wherever the reserve bound; blobs.money’s own formula still clamps the fee to the reserve.

Each transaction reports its toAddress besides its senderAddress. EIP-4844 forbids contract creation, so every blob transaction has one: usually a rollup’s inbox, which stays put while its batchers rotate. Version 1.6 adds it and reads the Body group for it.

evm-events 1.1.0

A declarative event decoder. Configuration supplies contract addresses, Solidity event ABI fragments, and output mappings. The factory parses and validates the ABI before a source is opened, derives exact topic-zero signatures, pushes address/topic predicates into capable sources, and decodes without EVM execution. It declares the Logs field group.

An event’s abi is a human-readable fragment, one JSON ABI event object as a block explorer shows it, or log0 (below). Fragments may use every ABI type Solidity events use except function and fixed-point: address, bool, bytes1..bytes32, uint/int widths from 8 through 256, bytes, string, fixed-size and dynamic arrays, and tuples, nested at most six levels deep. One element of a dynamic array holds at most 64 values, counting each nested array and tuple as one, because the decoder copies that layout for every element. A tuple is written with its component names, as (uint256 amount, bool exactOutput) params or tuple(…) params. Every parameter and tuple component needs a name (type name or type indexed name), because decoded values are keyed by name. Unnamed parameters, anonymous events, duplicate signatures, malformed keys, and more than three indexed fields fail deterministically during doctor. The order of the configured events is part of the configuration hash, so reordering them creates a new processor instance.

Decoded values are exact strings: integers in decimal; address, bytesN, and bytes as lowercase 0x hex; bool as true or false; and string as text, with invalid UTF-8 replaced by U+FFFD. A tuple that is not indexed flattens to one key per component, such as params.amount, and nested tuples extend the path. An array, also one inside a tuple, is one JSON string whose elements render as above, with tuple elements as objects in ABI order: ["1","2"] or [{"maker":"0x…","data":"0x01"}]. Ethereum stores an indexed string, bytes, array, or tuple only as the keccak hash of its value, so its value is that 32-byte topic as 0x hex. key_fields may name any value key, including a flattened one.

Any contract can emit a log with a configured event’s topic zero. A log whose topics or data do not decode as that event, such as an ERC-721 Transfer (four topics, no data) against the ERC-20 Transfer ABI or a bool word other than 0 or 1, is not that event: it is skipped rather than failing the block, and the block’s mapped delta counts the skipped logs. Only the canonical encoding decodes, the one Solidity’s encoder produces: offsets without gaps or shared tails, zero padding, and no trailing bytes. A log that breaks these rules, as a few old compilers’ event data did, is skipped and counted rather than guessed at. The layout is checked before anything decodes, so decoding takes memory in proportion to the log’s size.

abi = "log0" stores every topicless log of the configured contracts raw: the entity’s event and signature are log0, and its values hold one key, data, with the log data as 0x hex. Contracts such as Ekubo Core emit LOG0 with a packed layout that no ABI describes, so the application decodes those bytes. A log0 entry requires addresses, a processor may have one, and its key_fields may name data. A topicless log cannot match a topic-zero filter, so a processor with a log0 entry reads every log of its contracts and selects the configured events itself. An anonymous event with indexed parameters has topics but no signature, and is not decoded.

Without key_fields, an event’s key is its transaction hash and log index, which no other event shares, so the processor is block-local: its history and live blocks apply independently. A key_fields key can repeat across blocks, and its entity holds the key’s newest event, so any keyed output makes the processor ordered, like erc20-balances (ordered_state reduction and canonical delivery ordering). It reduces blocks strictly in chain order: a block emits one change per key, for its last event, and undoing a block restores the key’s event from before it. Its live lane therefore waits for its cold backfill, holding live blocks as pending deltas within [budgets] pending_delta_bytes until history reaches them. Consumers read a keyed instance’s stream through the generic consumer routes. Application subscriptions and POST /admin/v1/materialization-jobs require a block-local processor, so they refuse a keyed instance. With history_mode = "on_demand", its history comes from the backfill command instead, which applies an ordered processor’s history contiguously from start_block upward: each range must start at the block after the last one applied, and the command refuses any other start. Finalized-coverage compaction is block-local only, so a keyed instance keeps one coverage row per block it applies, without bound, as other ordered processors do.

publish = "finalized" holds unfinalized changes in the store until verified finality promotes them. The stream then contains only finalized apply records and system.finality markers; no undo is ever published. Held bytes count against [processors.delivery] max_bytes from admission.

[[processors]]
id = "evm-events"
instance = "weth-transfers"
version = "1.1.0"
start_block = 12965000
publish = "finalized"
[processors.state]
mode = "durable"
[processors.artifacts]
mode = "none"
[processors.output]
mode = "window"
[processors.output.window]
max_blocks = 216000
[processors.delivery]
mode = "none"
[processors.checkpoint]
mode = "automatic"
keep = 3
[processors.undo]
mode = "none"
safety_blocks = 0
[processors.settings]
addresses = ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"]
[[processors.settings.events]]
abi = "event Transfer(address indexed from, address indexed to, uint256 value)"
[processors.settings.events.output]
collection = "weth.transfers"
kind = "weth.transfer"
key_fields = []

Every entity/change uses evm.event.entity.v1/evm.event.change.v1, includes block and transaction identity as camelCase 0x-hex fields, and renders decoded values as exact decimal or hex strings. bucket_seconds plus _bucket_start in key_fields supports deterministic wall-clock buckets from canonical block timestamps.

transaction-stats 1.0.0

An ordered A-to-B aggregate that retains constant-sized private working state: transaction count, exact total value, cursor, checkpoints, and the bounded unfinalized contribution journal. It declares the Body and Senders field groups and sender/recipient predicates, and no receipt group, so it cannot tell whether a transaction reverted: count and totalValueWei cover every submitted transaction from the sender to the recipient. totalValueWei is the submitted value, not the value the recipient received.

[[processors]]
id = "transaction-stats"
instance = "treasury-flow-total"
version = "1.0.0"
start_block = 15537394
publish = "finalized"
[processors.state]
mode = "checkpointed"
[processors.artifacts]
mode = "none"
[processors.output]
mode = "latest"
[processors.delivery]
mode = "none"
[processors.checkpoint]
mode = "automatic"
keep = 3
[processors.undo]
mode = "none"
safety_blocks = 0
[processors.settings]
from = "0x1111111111111111111111111111111111111111"
to = "0x2222222222222222222222222222222222222222"
complete_from_start = true

Use output = "latest" for a continuing aggregate or [processors.delivery] emit = "job_result" for a bounded backfill. Job-result jobs commit state and coverage for every block but spool only the final aggregate; after acknowledgement the job is complete/reclaimable and deletion remains an explicit operator action.

rollup-txs 1.0.0

A block-local processor for rollups’ L1 transactions other than blobs: calldata batches, state root and output proposals, and proof submissions. It declares the Header, Body, Senders, Calldata and ReceiptOutcome field groups, filtered to the rules’ recipients. A transaction matches a rule when it is sent to to, from from when set, calls selector when set, carries chain_id_arg as its first ABI word when set (shared settlement contracts serving several chains), in a block whose timestamp lies in [since, until). The first matching rule per recipient wins, so list the more specific rules first. A rule also names the rollup its transactions belong to and their purpose (data, state, or proof), which the reported rows carry. Type-3 transactions never match: blobs-money reports them. Only top-level calls match; a call made through a multisig or another contract is not seen.

The processor supports Ethereum mainnet only, and only from London (block 12,965,000) on, because executionBurnedWei and tipWei need the base fee that earlier blocks lack. Startup refuses another chain and a pre-London start_block. since and until are UNIX timestamps in seconds, below 10^10 (the year 2286), not block numbers or milliseconds: a block matches from since (inclusive) up to until (exclusive). Startup refuses a since or until below mainnet’s genesis timestamp (1438269973), as any block number is, and one of 10000000000 or more, as any timestamp in milliseconds is: a millisecond until would never close its window, and a millisecond since would never match. It also refuses a window whose since is not below its until. A rule with chain_id_arg also needs a selector: the chain id is the argument after it, and without one the rows would render selector: null, so calls to different functions could not be told apart.

Every block emits one rollups.block change (rollups.block-bundle.v1), also when nothing matched, so a consumer can tell a block without rollup activity from one it never received. Materialized output stores each block’s bundle by block number in the rollups.blocks collection (rollups.entity.v1) and renders it as the same JSON. The bundle is camelCase JSON:

  • block: network, blockNumber, blockHash, parentHash, and timestamp. The bundle carries no finality of its own: the block’s finality is the finality of the change envelope, or of the retained entity, that carries the bundle.
  • transactions, in block order: network, blockNumber, and blockHash, repeated from the block so that each transaction stands alone as a row; then txHash, transactionIndex, rollupId and purpose (data, state, or proof), which are the matched rule’s rollup and purpose, success, senderAddress, toAddress, selector, gasUsed, executionBurnedWei = base fee × gas used, and tipWei = (effective price − base fee) × gas used.

A reverted transaction is reported like the others, with success: false, because its gas was paid. selector is the matched rule’s selector, or null when that rule has none: the first bytes of a calldata batch are batch data, not a function selector. Hashes, addresses, and selectors are 0x followed by lowercase hex digits: 64 for a hash, 40 for an address, and 8 for a selector. gasUsed, executionBurnedWei, and tipWei are decimal strings; blockNumber, timestamp, and transactionIndex are JSON numbers. The rules and the start_block are the processor’s identity: changing either needs a new instance.

[[processors]]
id = "rollup-txs"
instance = "rollup-txs-1"
version = "1.0.0"
history_control = "application_subscriptions"
history_mode = "on_demand"
start_block = 26000000
[processors.state]
mode = "durable"
[processors.output]
mode = "none"
[processors.delivery]
mode = "until_acknowledged"
[[processors.delivery.consumers]]
id = "app"
required = true
lease_ttl = "5m"
[processors.checkpoint]
mode = "automatic"
keep = 3
[processors.undo]
mode = "unfinalized"
safety_blocks = 256
[[processors.settings.rules]]
rollup = "base"
purpose = "state"
to = "0x43edb88c4b80fdd2adff2412a7bebf9df42cb40e"
selector = "0x82ecf2f6"

Xatu serves this processor’s history from its transaction projection, for the rule recipients (see data sources).

erc20-balances 1.1.0

An ordered watchlist ledger derived from canonical ERC-20 Transfer logs. Configuration declares watched addresses, an optional token allowlist, a start block, and whether the opening coverage is complete. It declares the Logs field group.

The stored method is explicitly erc20_transfer_ledger. Rebasing, reflection, and non-standard tokens are not represented as general EVM balances. Any contract can emit a Transfer log, so a log with that signature that is not a canonical ERC-20 transfer (three topics, zero-padded addresses, and a 32-byte value), such as an ERC-721 transfer, is skipped rather than failing the block; the block’s mapped delta counts the skipped logs.

A transfer the ledger cannot apply proves it incomplete for that token and holder: an outgoing transfer above the derived balance means transfers before the start block or outside the ERC-20 model, such as a rebasing or forged token, and an incoming one can overflow it. The pair is then marked incomplete: incompleteFrom holds that block, complete is false, balance keeps its last derived value, and later transfers for the pair are ignored. Undoing that block restores the derived pair. Only a token in the tokens allowlist with complete_from_start = true fails the processor instead, because the configuration declares that ledger complete.

[[processors]]
id = "erc20-balances"
instance = "erc20-watchlist"
version = "1.1.0"
start_block = 1234567
[processors.state]
mode = "durable"
[processors.output]
mode = "latest"
[processors.delivery]
mode = "window"
[processors.checkpoint]
mode = "automatic"
keep = 3
[processors.undo]
mode = "unfinalized"
safety_blocks = 256
[processors.coverage]
verification_segment_blocks = 8192
[processors.settings]
addresses = ["0x1111111111111111111111111111111111111111"]
tokens = ["0x2222222222222222222222222222222222222222"]
complete_from_start = true

block-summary 1.2.0

A receipt-free, block-local view of every Ethereum execution block. It declares the Header and Body field groups, then publishes timestamp, parent/hash identity, gas limit and usage, decimal base fee, blob gas fields, transaction count, exact execution-block size when available, and finality. Blocks come from verified material or, under the trusted-dataset policy, from Xatu’s dataset-declared header and transaction projections, whose body indices and payload transaction count must agree. Dataset rows carry no execution-block size, so sizeBytes is null for them. A block a dataset summarized first matches the same block from verified material, as when the live lane overlaps a Xatu backfill, and keeps null. The reverse, remapping a block that verified material summarized from a dataset, as a recompute may, is refused as a conflicting apply. Summaries are retained by block hash with a canonical block-number index; the query extension exposes /latest and /blocks/{number}. It never executes the EVM and does not request receipts.

The compact [blocks] preset supplies the standard checkpointed lifecycle, full query output, 64 MiB/24 hour delivery window, and undo records kept until 256 blocks past finality. Advanced configurations can override those policies explicitly.

uniswap-observations 2.1.0 / uniswap-latest 2.0.0

Two configured-pool contracts share the same exact event decoder:

  • Uniswap V2 Sync(uint112,uint112) reserve observations;
  • Uniswap V3 Initialize(uint160,int24) and Swap(address,address,int256,int256,uint160,uint128,int24) square-root-price observations.

They store exact reserves and sqrtPriceX96, never floating-point prices. V3 swap observations also retain amount0 and amount1 as signed decimal int256 values, preserving the exact pool deltas needed to derive volume. Applications combine these values with verified token metadata. uniswap-observations is block-local and identifies immutable observations by pool, block hash, and log index; it supports independent history/live delivery and full or windowed SQLite retention. Version 2.1 also maintains a query-only latest-observation pointer per pool so new clients can bootstrap immediately before following the immutable stream. Transaction hashes are deliberately not part of this processor’s output, allowing archive and P2P sources to omit transaction bodies when acquiring filtered pool logs. Both processors declare the Logs field group. uniswap-latest is ordered/canonical and retains only the latest observation per pool for slim price queries.

[[processors]]
id = "uniswap-observations" # or "uniswap-latest"
instance = "usdc-weth-observations"
version = "2.1.0" # use 2.0.0 with uniswap-latest
start_block = 12369621
history_control = "node_owned"
history_mode = "on_demand"
[processors.state]
mode = "checkpointed"
[processors.artifacts]
mode = "none"
[processors.output]
mode = "full"
[processors.delivery]
mode = "none"
[processors.checkpoint]
mode = "automatic"
keep = 3
[processors.undo]
mode = "unfinalized"
safety_blocks = 256
[processors.settings]
[[processors.settings.pools]]
address = "0x3333333333333333333333333333333333333333"
kind = "v3"

Pool factory discovery is intentionally separate from event reduction. A catalog adapter can discover pools and render these immutable processor configs; each processor identity then commits to the exact pool set.