# List Ink pools at one block hash: EIP-1898 eth_call and Multicall3

> Original page: https://docs.blockvectra.com/en/guides/ink-pool-discovery/

## Direct answer

To discover pools on Ink Mainnet at a single block, use the authenticated endpoint `https://api.blockvectra.com/v1/ink_mainnet` with your key in the `x-api-key` header. Read the latest block once, then send every later `eth_call` with that block's hash as an [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898) selector, `{"blockHash": "0x…", "requireCanonical": true}`. The keyless `…/v1/ink_mainnet/public` route rejects this hash-pinned `eth_call` with `-32011` and reason `state_window` (see [When a hash-pinned read is rejected](#when-a-hash-pinned-read-is-rejected)), so this task needs a key.

Keep the key in the `BLOCKVECTRA_API_KEY` environment variable on your machine. Do not put it in a URL, a chat message, source code or a log line. To get one, [log in to the console](https://console.blockvectra.com/login/?next=%2Fkeys%2F), or follow the [programmatic sign-up guide](https://docs.blockvectra.com/en/guides/programmatic-signup/) from an agent or script. This page does not create accounts or keys.

* **First step:** check that `eth_chainId` returns `0xdef1` (57073).
* **Done when:** the script prints one block hash, the factory's pool count at that hash and exactly that many pool addresses.

## The four requests

1. `eth_chainId` must equal 57073. Stop on anything else.
2. `eth_getBlockByNumber` with `latest` returns the head. Keep its `hash`; do not use the block number from now on.
3. `eth_call` to the factory `0x7660BF2aDefC687C22906112ca323d60A00358cF` with `allPoolsLength()` (selector `0xefde4e64`) at `{blockHash, requireCanonical: true}` returns the pool count.
4. One `eth_call` to Multicall3 `0xcA11bde05977b3631167028862bE2a173976CA11` at the same hash selector runs `aggregate3` over `allPools(0)` … `allPools(count − 1)` (selector `0x41d1de97`). Build the list from the count you just read; do not hard-code it.

## Example: Node.js and viem

This example needs Node.js 20 or later and viem 2.x. Install viem yourself in your own project with `npm install viem`; nothing else is required. Save the script as `ink-pools.mjs`:

```bash
export BLOCKVECTRA_API_KEY='replace-with-your-key'
node ink-pools.mjs
```

```js
import { decodeAbiParameters, decodeFunctionResult, encodeFunctionData, parseAbi, zeroAddress } from "viem";

const ENDPOINT = "https://api.blockvectra.com/v1/ink_mainnet";
const CHAIN_ID = 57073n;
const FACTORY = "0x7660BF2aDefC687C22906112ca323d60A00358cF";
const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
const MAX_POOLS = 150n; // example budget for this script, not a product limit
const TIMEOUT_MS = 15_000;
const GAP_MS = 1_100;

const apiKey = process.env.BLOCKVECTRA_API_KEY;
if (!apiKey || apiKey === "replace-with-your-key") throw new Error("Set BLOCKVECTRA_API_KEY in your local environment");

const factoryAbi = parseAbi([
  "function allPoolsLength() view returns (uint256)",
  "function allPools(uint256) view returns (address)",
]);
const multicallAbi = parseAbi([
  "function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)",
]);

// One request at a time, no retry, no redirect, no fallback to another endpoint.
let lastSentAt = 0;
async function rpc(method, params) {
  const wait = lastSentAt + GAP_MS - Date.now();
  if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
  lastSentAt = Date.now();
  const response = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "content-type": "application/json", "x-api-key": apiKey },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
    redirect: "error",
    signal: AbortSignal.timeout(TIMEOUT_MS),
  });
  if (response.status !== 200) throw new Error(`${method}: HTTP ${response.status}`);
  const body = await response.json();
  if (body?.jsonrpc !== "2.0" || body.id !== 1) throw new Error(`${method}: invalid JSON-RPC response`);
  if (body.error) {
    const reason = String(body.error.data?.reason ?? "").slice(0, 64);
    throw new Error(`${method}: JSON-RPC error ${body.error.code} ${reason}`.trim());
  }
  return body.result;
}

function requireData(value, bytes) {
  const valid = typeof value === "string" && /^0x(?:[0-9a-f]{2})*$/i.test(value)
    && (bytes === undefined || value.length === 2 + bytes * 2);
  if (!valid) throw new Error("Invalid eth_call result");
  return value;
}

// 1. Chain id
const chainIdHex = await rpc("eth_chainId", []);
if (typeof chainIdHex !== "string" || !/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(chainIdHex)) throw new Error("Invalid chain id");
if (BigInt(chainIdHex) !== CHAIN_ID) throw new Error(`Expected chain ${CHAIN_ID}, got ${BigInt(chainIdHex)}`);

// 2. Read the head once and keep only its hash
const head = await rpc("eth_getBlockByNumber", ["latest", false]);
const blockHash = head?.hash;
if (typeof blockHash !== "string" || !/^0x[0-9a-f]{64}$/i.test(blockHash)) throw new Error("Invalid block hash");
const at = { blockHash, requireCanonical: true };

// 3. Pool count at that hash
const countData = await rpc("eth_call", [
  { to: FACTORY, data: encodeFunctionData({ abi: factoryAbi, functionName: "allPoolsLength" }) },
  at,
]);
const count = decodeFunctionResult({ abi: factoryAbi, functionName: "allPoolsLength", data: requireData(countData, 32) });
if (count < 1n || count > MAX_POOLS) throw new Error(`allPoolsLength ${count} is outside 1..${MAX_POOLS}`);
const size = Number(count);

// 4. allPools(i) for every i below the count, in one aggregate3 at the same hash
const calls = Array.from({ length: size }, (_, i) => ({
  target: FACTORY,
  allowFailure: true,
  callData: encodeFunctionData({ abi: factoryAbi, functionName: "allPools", args: [BigInt(i)] }),
}));
const listData = await rpc("eth_call", [
  { to: MULTICALL3, data: encodeFunctionData({ abi: multicallAbi, functionName: "aggregate3", args: [calls] }) },
  at,
]);
const results = decodeFunctionResult({ abi: multicallAbi, functionName: "aggregate3", data: requireData(listData) });
if (results.length !== size) throw new Error(`aggregate3 returned ${results.length} results for ${size} calls`);

const pools = results.map(({ success, returnData }, i) => {
  if (!success) throw new Error(`allPools(${i}) failed inside Multicall3`);
  const word = requireData(returnData, 32);
  if (!/^0x0{24}[0-9a-f]{40}$/i.test(word)) throw new Error(`allPools(${i}) returned non-zero address padding`);
  const [pool] = decodeAbiParameters([{ type: "address" }], word);
  if (pool === zeroAddress) throw new Error(`allPools(${i}) returned the zero address`);
  return pool;
});
if (new Set(pools.map((pool) => pool.toLowerCase())).size !== pools.length) throw new Error("Duplicate pool address");

console.log(JSON.stringify({ blockHash, count: size, pools }, null, 2));
```

The script stops with a non-zero exit code and no partial output when any of these happens:

* an HTTP status other than 200, a JSON-RPC `error`, a 15 second timeout or a redirect;
* a chain id other than 57073, a malformed block hash or a result that is not valid hex or valid ABI data;
* a count outside 1 to 150. The 150 cap is only a bounded budget for this example; it is not a product limit, and we have not tested a 150-call batch here;
* an inner `allPools(i)` call that fails, or a zero or duplicate pool address.

It makes four calls, about 1.1 seconds apart, never retries, never broadcasts a transaction, never pages and never substitutes `latest` or a block number for the hash. It prints only the hash, the count and the pool addresses, never headers or the key.

## Sample run

Our QA ran these four requests against `ink_mainnet` with a key on 2026-11-03 at 06:40 UTC. Every response was HTTP 200 with no retry:

| Item                            | Observed                                                                            |
| ------------------------------- | ----------------------------------------------------------------------------------- |
| Chain id                        | `0xdef1` (57073)                                                                    |
| Block                           | 60189593, hash `0xc3b4349618611b7dfd979140a70087920cb8416eb1c8eff82cb46332494b7617` |
| `allPoolsLength()` at that hash | 17                                                                                  |
| `aggregate3` at that hash       | 17 results, all successful, non-zero and unique                                     |

This is one QA run, not a current value: the head moves and the factory's count can grow, so read both again when you run it. It does not show a full Pharos run, a 150-call batch, pool balances or TVL, sustained use or adoption by any team. The [saved requests and responses](https://gist.github.com/gjwang/5ce60ad10587040cbe2d743dd0a3e149/a68a3c917e4a272b9131bd42c30c875b55ff27a5#file-20261103-keyed-hash-pool-discovery-json) are public.

## When a hash-pinned read is rejected

On 2026-11-03 the keyless route returned the head, then rejected an `eth_call` with `{blockHash, requireCanonical: true}` using JSON-RPC `-32011` and `error.data.reason: state_window`. That error is not retryable (see [state\_window](https://docs.blockvectra.com/en/errors/#state_window) and [historical state windows](https://docs.blockvectra.com/en/guides/evm-historical-state/)). The authenticated route and the keyless route are different entry points; check `public.methods` and the state window in [GET /v1/chains](https://api.blockvectra.com/v1/chains) rather than assuming one behaves like the other.

If a hash-pinned call is rejected, because the block is no longer canonical or the serving node cannot serve it:

* Stop. Do not repeat the same hash, and do not swap in `latest` or a block number to get an answer. The count and the pool list would then come from different blocks.
* Start a new run from step 2. Its output is a new snapshot with its own hash.

Two reads accepted at the same hash do not prove the block can never be reorganized later, and they do not validate the rest of your application. Treat the output as a snapshot at that hash. For the method itself, see [eth\_call](https://docs.blockvectra.com/en/api/json-rpc/methods/eth_call/); for Ink Mainnet parameters and access options, see the [Ink chain page](https://blockvectra.com/en/chains/ink_mainnet/).

## Independent public-RPC observations

If you also want an outside view of public Ink RPC endpoints, OpenChainBench publishes one at [openchainbench.com/benchmarks/ink-rpc](https://openchainbench.com/benchmarks/ink-rpc), with the same data as JSON at [openchainbench.com/api/stat/ink-rpc](https://openchainbench.com/api/stat/ink-rpc). It measures a keyless `eth_getBlockByNumber("latest", false)` every 60 seconds from three regions. That is a different workload from this guide's keyed, hash-pinned `eth_call` and Multicall3 requests, and it says nothing about whether pool discovery succeeds, so run the example with your own key before relying on it.

## Next steps

* [See the free plan and pricing](https://blockvectra.com/en/pricing/#free) to check what your account includes.
* [Log in to the console](https://console.blockvectra.com/login/?next=%2Fkeys%2F) to create an API key.
