List Ink pools at one block hash: EIP-1898 eth_call and Multicall3
Read an Ink factory's pool list pinned to one canonical block hash: eth_chainId, the latest block hash, allPoolsLength, then Multicall3 aggregate3, with an x-api-key header and a Node.js viem example.
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 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), 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, or follow the programmatic sign-up guide from an agent or script. This page does not create accounts or keys.
- First step: check that
eth_chainIdreturns0xdef1(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
eth_chainIdmust equal 57073. Stop on anything else.eth_getBlockByNumberwithlatestreturns the head. Keep itshash; do not use the block number from now on.eth_callto the factory0x7660BF2aDefC687C22906112ca323d60A00358cFwithallPoolsLength()(selector0xefde4e64) at{blockHash, requireCanonical: true}returns the pool count.- One
eth_callto Multicall30xcA11bde05977b3631167028862bE2a173976CA11at the same hash selector runsaggregate3overallPools(0)…allPools(count − 1)(selector0x41d1de97). 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:
export BLOCKVECTRA_API_KEY='replace-with-your-key'
node ink-pools.mjsimport { 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 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 and historical state windows). The authenticated route and the keyless route are different entry points; check public.methods and the state window in GET /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
latestor 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; for Ink Mainnet parameters and access options, see the Ink chain page.
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, with the same data as JSON at 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 to check what your account includes.
- Log in to the console to create an API key.
Last updated:
One key, many chains
The same API key works on every supported chain. Learn how URLs are structured, how to discover chains programmatically, and how balances and limits pool.
Webhook push
Create one HTTP subscription with your HTTPS URL, chains and up to 1,000,000 addresses; native, token and log matches are POSTed with a signed body at least once. Verify signatures, deduplicate event IDs and replay retained matches.