Skip to content
Integration guide View as Markdown

Index eventsLink to this section

TL;DRLink to this section

  • SqueekCore logs record settled work and block progress; SqueekMining and SqueekToken logs record funded rewards, activations and claims.
  • Decode with the ABI of the implementation active at that address and block.
  • BatchSettled advances the certified frontier even when no round recorded work.
  • Store block hashes and log order so a reorg can roll back derived state, not just raw logs.

Before you startLink to this section

Complete Verify the deployment and start at INDEX_FROM_BLOCK from Deployments. Decode with the published ABIs linked there. The graph launches fresh: there is no earlier ledger to continue from.

Applications log at the proxy addresses. A matching topic does not prove the emitter, so filter by verified address and keep each proxy's implementation history, because an upgrade can change the ABI.

StepsLink to this section

Fetch and order logsLink to this section

  1. Fetch logs for the four verified proxies in bounded Ethereum block ranges.
  2. Store chain ID, block number and hash, transaction hash and index, and log index. Use (chainId, blockHash, transactionHash, logIndex) as the occurrence identity.
  3. Process in block, transaction index and log index order.
  4. After an upgrade event, verify the new implementation before choosing its ABI. Stop the projection if the code or layout is unknown.

Know which events each action emitsLink to this section

Action Events, in order
Settlement Zero or more SqueekCore RoundRecorded, then one BatchSettled
Mint For each Squeek, SqueekNFT Transfer from zero then SqueekMining PositionRegistered; then one SqueekNFT batch event (PublicMinted, AllowlistMinted, ProtocolReserveMinted or PublicRemainderMinted)
Checkpoint SqueekMining WeightActivated and Checkpointed per stretch, then one SqueekToken mint Transfer and CheckpointMinted
Claim SqueekMining RewardsClaimed; a SqueekToken Transfer only when the payout is nonzero
Merge SqueekNFT burn Transfer for the donor, then SqueekMining PositionsMerged
NFT transfer SqueekNFT Transfer only; SqueekMining gets no callback

Index ownership from SqueekNFT Transfer events, not SqueekMining events. A reverted transaction emits nothing; a reorg can remove events from a receipt you already saw.

Index SqueekCoreLink to this section

  1. Start physical work and block units at zero and difficulty at CoreInitialized.initialWorkPerThousandBlocks.
  2. For each RoundRecorded, price its work with the previous position's difficulty, then check the emitted totals and the emitted next difficulty.
  3. Advance the certified frontier on every BatchSettled, including batches with zero active rounds. Reconcile its final totals and activeRoundCount with the transaction's RoundRecorded events, which all precede it.
  4. Derive completed blocks from the change in cumulativeBlockUnits / 1_000_000_000. There is no per-block event, and one settlement can complete several blocks.
// decode-round.ts — `log` is a raw log already filtered to the verified SqueekCore proxy
import { decodeEventLog, parseAbi, type Log } from "viem";

const abi = parseAbi([
  "event RoundRecorded(uint64 indexed roundId, uint64 work, uint64 cumulativeWork, uint96 cumulativeBlockUnits, uint32 workPerThousandBlocks, bytes32 dataHash)",
]);
export function decodeRound(log: Log) {
  const { args } = decodeEventLog({ abi, data: log.data, topics: log.topics, strict: true });
  return {
    round: args.roundId,
    physicalWork: args.cumulativeWork,
    completedBlocks: args.cumulativeBlockUnits / 1_000_000_000n,
    blockProgress: args.cumulativeBlockUnits % 1_000_000_000n,
    nextDifficulty: BigInt(args.workPerThousandBlocks),
  };
}

The emitted difficulty prices the next active round, not this one. Follow the price-then-steer rule with exact integer arithmetic.

Index SqueekMining and SqueekTokenLink to this section

  1. Record PositionRegistered for mints and PositionsMerged for merges; together with SqueekNFT Transfer they give the live position set.
  2. For each checkpoint, record every Checkpointed stretch and WeightActivated, then the single SqueekToken mint Transfer and CheckpointMinted.
  3. Count new supply once, from the mint Transfer or from Checkpointed.amount; CheckpointMinted describes the same SQK.
  4. Record RewardsClaimed for payouts. Its SQK Transfer exists only when amount > 0.
  5. Reconcile Checkpointed.minted with SqueekToken totalSupply() at the same block.

Replay and reconcileLink to this section

Replay both contracts from the deployment block to one confirmed block, then compare the result with each contract's own state at that block. A mismatch means a missed log, a wrong decoder, or a reorg; never adjust the projection to match.

  1. Fetch RoundRecorded and BatchSettled from SqueekCore and Checkpointed from SqueekMining in bounded ranges, oldest first.
  2. Require each BatchSettled.previousSettledThroughRound to equal the frontier you hold, so no settlement is skipped.
  3. Sum activeRoundCount across batches; it must equal the number of RoundRecorded events.
  4. Sum Checkpointed.amount; it must equal SqueekMining state().minted and SqueekToken totalSupply().
// replay.ts
import { parseAbi, parseAbiItem } from "viem";
import { blockNumber, client, required } from "./setup";
import { verifyGraph } from "./verify";

const { core, mining, token } = await verifyGraph();
const start = BigInt(required("INDEX_FROM_BLOCK"));
const roundRecorded = parseAbiItem("event RoundRecorded(uint64 indexed roundId, uint64 work, uint64 cumulativeWork, uint96 cumulativeBlockUnits, uint32 workPerThousandBlocks, bytes32 dataHash)");
const batchSettled = parseAbiItem("event BatchSettled(bytes32 indexed digest, address indexed reporter, uint64 indexed settledThroughRound, uint256 nonce, uint64 previousSettledThroughRound, uint16 activeRoundCount, uint64 cumulativeWork, uint96 cumulativeBlockUnits)");
const checkpointed = parseAbiItem("event Checkpointed(uint64 indexed afterRound, uint64 indexed throughRound, uint64 completedBlocks, uint64 cumulativeWork, uint96 cumulativeBlockUnits, uint256 amount, uint256 minted, uint64 lastActivationRound, uint256 totalWeight, uint256 rewardIndex)");

let frontier = 0n, blockUnits = 0n, activeRounds = 0n, recorded = 0n, minted = 0n;
for (let from = start; from <= blockNumber; from += 1_000n) {
  const to = from + 999n < blockNumber ? from + 999n : blockNumber;
  const range = { fromBlock: from, toBlock: to, strict: true } as const;
  const [rounds, batches, checkpoints] = await Promise.all([
    client.getLogs({ address: core, event: roundRecorded, ...range }),
    client.getLogs({ address: core, event: batchSettled, ...range }),
    client.getLogs({ address: mining, event: checkpointed, ...range }),
  ]);
  recorded += BigInt(rounds.length);
  for (const { args } of batches) {
    if (args.previousSettledThroughRound !== frontier) throw new Error(`Missing settlement before round ${args.settledThroughRound}`);
    frontier = args.settledThroughRound;
    blockUnits = args.cumulativeBlockUnits;
    activeRounds += BigInt(args.activeRoundCount);
  }
  for (const { args } of checkpoints) minted += args.amount;
}

const views = parseAbi([
  "function state() view returns ((uint64 settledThroughRound, uint64 cumulativeWork, uint96 cumulativeBlockUnits, uint64 blockHeight, uint96 blockProgress, uint32 workPerThousandBlocks, uint256 authorizationNonce, bool paused))",
  "function totalSupply() view returns (uint256)",
]);
const miningState = parseAbi([
  "function state() view returns ((uint256 minted, uint256 paid, uint256 totalWeight, uint256 rewardIndex, uint64 closedThroughBlock, uint64 lastActivationRound, uint256 weightActivationCursor, uint256 weightActivationCount, bool bootstrapped, uint64 processedThroughRound, uint64 closedCumulativeWork, uint96 closedCumulativeBlockUnits, uint32 closedWorkPerThousandBlocks))",
]);
const [coreState, mined, supply] = await Promise.all([
  client.readContract({ address: core, abi: views, functionName: "state", blockNumber }),
  client.readContract({ address: mining, abi: miningState, functionName: "state", blockNumber }),
  client.readContract({ address: token, abi: views, functionName: "totalSupply", blockNumber }),
]);
const checks = {
  frontier: frontier === coreState.settledThroughRound,
  blockUnits: blockUnits === coreState.cumulativeBlockUnits,
  activeRounds: activeRounds === recorded,
  minted: minted === mined.minted && minted === supply,
};
if (Object.values(checks).includes(false)) throw new Error(`Projection disagrees with contract state: ${JSON.stringify(checks)}`);
console.log({ blockNumber, frontier, completedBlocks: blockUnits / 1_000_000_000n, recorded, mintedBaseUnits: minted });

A production indexer stores each log with its block hash and applies the same checks continuously instead of replaying from the start.

Handle reorgsLink to this section

Keep unconfirmed head data separate from confirmed projections. When a stored block hash no longer matches the chain, roll derived state back to the common ancestor and replay from there. Deduplicating raw log IDs alone leaves derived totals wrong.

Expected resultLink to this section

Event Projection effect
CoreInitialized Stream, actors, genesis, work cap and initial difficulty; totals start at zero
RoundRecorded One active round's work and hash, with the resulting physical total, earned total and next difficulty
BatchSettled New certified frontier, consumed nonce, active count, and final totals
PositionRegistered, PositionsMerged New and merged positions with their pending power and cooldowns
Checkpointed, WeightActivated Funded stretches with completed blocks and amounts; power changes applied
SqueekToken mint Transfer, CheckpointMinted One aggregate mint per checkpoint
RewardsClaimed Payout to the owner and the remainder left on the lowest ID
Pause, Reporter and upgrade events Availability, signer and implementation history

Exact field lists are on each contract page: SqueekCore, SqueekMining, SqueekToken and SqueekNFT.

Common failuresLink to this section

Failure Cause Fix
Wrong topics or tuple widths Decoding with an earlier ABI Select the ABI per implementation and block
Earned totals drift Pricing a round with its own emitted difficulty Use the previous position's difficulty
Block count is off Dividing physical work by current difficulty Use block units
Frontier lags Ignoring zero-record BatchSettled Advance on every BatchSettled
Supply doubled Counting CheckpointMinted and its Transfer separately Count one of them
Gaps read as zero work No RoundRecorded outside a certified interval Treat uncertified rounds as unknown
Totals wrong after a reorg Deduplicating logs without rolling back Roll back to the common ancestor and replay

A dataHash commits to the round's supporting records; it is not those records or proof of physical causation.

Verify the resultLink to this section

At one canonical block, compare your replayed frontier, physical total, earned total and difficulty with SqueekCore.state(), and selected endpoints with roundPage(r, r, 0). Compare your minted and paid totals with SqueekMining state() and SqueekToken totalSupply().

BatchSettled.nonce is the nonce that settlement consumed, not the next one. Read state() at the same block before preparing a new authorization.