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.

Method
Filter
Join
Order

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

tx 0xa1: referenced 2×, listed onceblock 0x65: referenced 3×, listed once
{
  "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.

Target
Server budget
Order
40
0
55
30
12
0
0
70
25
8
60
1011121314151617181920

    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.

    BlockStored by the clientChain (hash, parent)
    100 finalized0x1ahash 0x1aparent 0x09
    1010x2bhash 0x2bparent 0x1a
    1020x3chash 0x3cparent 0x2b
    1030x4dhash 0x4dparent 0x3c
    104cursorBlock0x5ehash 0x5eparent 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