Query chain history in pages
5 methods
eth_queryBlocks, Transactions, Logs, Traces and Transfers.
1 request
for logs plus their transactions and blocks. Each related object comes back once.
Whole blocks
per page. A page never ends halfway through a block.
MIP-16 is a Draft. It adds five JSON-RPC methods for reading chain history with server-side filters, field selection and joins. No Monad client release serves them yet: the public RPC answers eth_queryLogs with -32601, method not found.
The information on this page should not be quoted. Please refer to MIP-16 for the authoritative spec.
What takes many calls today
The spec names four gaps in the standard JSON-RPC interface. The numbers below come from a made-up range of 1,000 blocks.
240 Transfer logs, plus each one's transaction input and block timestamp. They sit in 180 transactions across 150 blocks.
Today
331+calls
eth_getLogs, then eth_getTransactionByHash for each of the 180 transactions and eth_getBlockByNumber for each of the 150 blocks.
With MIP-16
1calls
eth_queryLogs with fields for logs, transactions and blocks.
Counts assume each MIP-16 query fits in one page. Today's counts assume eth_getLogs covers the range in one call; many providers cap the range, which adds calls.
Build a request
All five methods take the same request shape. Pick a method, then toggle the filter and the joins. The response is computed from a small sample chain of three blocks.
Logs use the same address and positional topics filter as eth_getLogs.
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_queryLogs",
"params": [
{
"fromBlock": "0x65",
"toBlock": "0x67",
"order": "asc",
"filter": { "address": "usdc", "topics": ["Transfer"] },
"fields": {
"logs": ["blockNumber", "logIndex", "transactionHash", "address"],
"transactions": ["hash", "input"],
"blocks": ["number", "timestamp"]
}
}
]
}Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"data": {
"logs": [
{ "blockNumber": "0x65", "logIndex": "0x0", "transactionHash": "0xa1", "address": "usdc" },
{ "blockNumber": "0x65", "logIndex": "0x2", "transactionHash": "0xa1", "address": "usdc" },
{ "blockNumber": "0x65", "logIndex": "0x3", "transactionHash": "0xa2", "address": "usdc" },
{ "blockNumber": "0x66", "logIndex": "0x0", "transactionHash": "0xb1", "address": "usdc" },
{ "blockNumber": "0x67", "logIndex": "0x0", "transactionHash": "0xc1", "address": "usdc" }
],
"transactions": [
{ "hash": "0xa1", "input": "swap(…)" },
{ "hash": "0xa2", "input": "transfer(…)" },
{ "hash": "0xb1", "input": "transfer(…)" },
{ "hash": "0xc1", "input": "swap(…)" }
],
"blocks": [
{ "number": "0x65", "timestamp": "0x6ace3960" },
{ "number": "0x66", "timestamp": "0x6ace3961" },
{ "number": "0x67", "timestamp": "0x6ace3961" }
]
}
}
}Related objects come back in their own arrays, each one once, and only if a primary object in the response points to it. Join them on blockNumber and transactionHash, so select those fields on both sides.
Addresses and the Transfer topic are shown as names, and the calldata is shortened. The cursor fields (fromBlock, toBlock, cursorBlock) are left out here; the next section covers them.
Pages end on block boundaries
The spec's example: blocks 10 to 20, target 100. Each bar is one block, sized by how many objects match in it. Step through the pages.
To continue, repeat the request with only fromBlock changed: cursorBlock + 1 going up, cursorBlock - 1 going down. Pages never split a block, so nothing is repeated or skipped.
The budget is whatever bound the server puts on one response: time, size or object count. It's modeled here as an object count.
Spotting a reorg between pages
Every response carries fromBlock, toBlock and cursorBlock as number, hash and parent hash. A client paging toward the tip compares them.
| Block | Stored by the client | Chain (hash, parent) |
|---|---|---|
| 100 finalized | 0x1a | hash 0x1a · parent 0x09 |
| 101 | 0x2b | hash 0x2b · parent 0x1a |
| 102 | 0x3c | hash 0x3c · parent 0x2b |
| 103 | 0x4d | hash 0x4d · parent 0x3c |
| 104cursorBlock | 0x5e | hash 0x5e · parent 0x4d |
Page 1 covers blocks 100 to 104. The client stores the number and hash of every block it processes from the finalized block onward.
A finalized block can't be replaced, so storing references back to it means one always matches. Within one response, every object comes from the same chain.
Which error, when
Five codes mark the boundaries. The first two tell a client whether to look for another node.
-32601
Method not found
The node's software predates MIP-16. This is what Monad's public RPC returns today.
-32004
Method not supported
The node knows the method but isn't set up to serve it. A node that doesn't index traces returns this for eth_queryTraces and eth_queryTransfers.
-32602
Invalid params
Malformed request: an unknown fields key, a bad filter, an unsupported relation, a target of 0x0, or a range that runs the wrong way for the order.
-32001
Resource not found
Part of the range is outside what this node holds for this method: pruned, never indexed, or not produced yet. The node must fail instead of returning part of the range.
-32005
Limit exceeded
The server's budget ran out before it finished fromBlock.
Each method has its own availability window on a node. A node can keep every block back to genesis and only recent traces.
Who this affects
Indexers and data pipelines
Fewer calls, simpler backfills
Server-side filters cover transactions, traces and native transfers. Joins replace the N+1 lookups, and block-aligned pages with block hashes give a cursor and reorg detection without extra requests.
Node operators and RPC providers
New indexes and request limits
Serving these methods needs indexed history, and a node may offer only some of them. The spec expects limits on response size or execution time plus rate limits and per-key quotas, since joins and field selection change the worst-case cost of a request.
App developers
Nothing changes until a node serves them
These are five new methods in the eth_ namespace. No existing method changes, and the spec describes nothing Monad-specific, so other EVM chains could adopt them.
MIP-16 is a Draft. An open pull request (monad-crypto/MIPs #89) would move it to Review and change some details: "all" instead of true to select every field, an includeReverted filter instead of isTopLevel for traces and transfers, a reverted field on traces, and DELEGATECALL and CALLCODE frames no longer counting as transfers. The block-aligned pages, joins and error codes shown here are the same in both versions. On the client side, a monad-bft pull request (#2945) that starts the indexing backend is open.
Continue the discussion on Monad Forum
Questions, feedback, or a better idea? Weigh in on the forum thread.
Open forum thread