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.
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.
Fetch logs for the four verified proxies in bounded Ethereum block ranges.
Store chain ID, block number and hash, transaction hash and index, and log index. Use (chainId, blockHash, transactionHash, logIndex) as the occurrence identity.
Process in block, transaction index and log index order.
After an upgrade event, verify the new implementation before choosing its ABI. Stop the projection if the code or layout is unknown.
| 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.
Start physical work and block units at zero and difficulty at CoreInitialized.initialWorkPerThousandBlocks.
For each RoundRecorded, price its work with the previous position's difficulty, then check the emitted totals and the emitted next difficulty.
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.
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.
Record PositionRegistered for mints and PositionsMerged for merges; together with SqueekNFT Transfer they give the live position set.
For each checkpoint, record every Checkpointed stretch and WeightActivated, then the single SqueekToken mint Transfer and CheckpointMinted.
Count new supply once, from the mint Transfer or from Checkpointed.amount; CheckpointMinted describes the same SQK.
Record RewardsClaimed for payouts. Its SQK Transfer exists only when amount > 0.
Reconcile Checkpointed.minted with SqueekToken totalSupply() at the same block.
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.
Fetch RoundRecorded and BatchSettled from SqueekCore and Checkpointed from SqueekMining in bounded ranges, oldest first.
Require each BatchSettled.previousSettledThroughRound to equal the frontier you hold, so no settlement is skipped.
Sum activeRoundCount across batches; it must equal the number of RoundRecorded events.
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.
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.
| 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.
| 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.
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.
Applies SqueekCore or SqueekMining upgrades authorized by its governing Timelock.
Generates Squeek images and metadata from fixed onchain visual data.
Records physical work and earned block units on Ethereum, with difficulty adapting after active rounds.
Calculates rewards from settled work, holds funded SQK, and pays eligible NFT owners.
The Squeeks ERC-721 contract. Tracks NFT ownership and coordinates minting and merges with SqueekMining.
The SQK ERC-20 contract. Enforces the lifetime supply cap; only SqueekMining can mint SQK.
Queues governance operations and enforces a delay before they can be executed.
Checks that visual data conforms to the required DotGrid format.
When new power starts earning. Minting and merging schedule it for the next round, and a checkpoint applies it.
A reward update that processes settled Squeek blocks, issues their SQK, and applies scheduled power changes.
The Squeek permanently burned in a merge. Its unclaimed rewards move to the survivor, but its image is not combined or kept.
SQK issued as settled Squeek blocks are processed: 5,600 SQK for each completed block, up to the lifetime cap.
Squeekchain's record of accepted wheel-magnet passages.
Combining two eligible same-level Squeeks you own into one higher-level Squeek. One NFT is burned; the survivor keeps both Squeeks' unclaimed rewards.
The completed Squeek blocks a Squeek waits through before it can merge again. It follows the Squeek through transfers.
What a Squeek earns with. Its level sets its maximum power; each Squeek block's SQK is divided across all active power in proportion.
A ten-minute period used to group measured work. The device saves a completed round's work total when measurement remained uninterrupted.
The token each completed Squeek block issues, shared by active power. It has a lifetime supply cap.
An NFT that owns a share of the Squeekchain network. It receives SQK from the hamster's work according to its active power, set by its level and the SQK staked to it.
One billion block units earned from settled work at each round's difficulty. Completed blocks supply SQK under the mining reward rules.
Squeekchain's ecosystem for hamster-powered onchain finance, built on the public work record.
The Squeek you keep in a merge. It gains a level, keeps its ID and image, and receives both Squeeks' unclaimed rewards.
A Squeek's recorded reward balance that hasn't been paid to a wallet. It stays with the Squeek when ownership changes.
The hamster mining device. It records accepted wheel-magnet passages as work.
One wheel-magnet passage that met the device's counting rules.