--- name: ethereum-rpc-scraper description: Read Ethereum, Sepolia, Base and Arbitrum One straight from JSON-RPC via the Apify Actor arman-bd/ethereum-rpc-scraper. Returns one row per query: current gas price with EIP-1559 base fee, next-block base fee and priority tip; the chain head; native balances and nonces per address at any block tag; and full block headers with hash, parent, timestamp, proposer, gas used and size. All wei values are exact decimal strings, never floats. Use when a task needs gas monitoring, treasury or wallet balance tracking, chain-head or block-header alerting without running an indexer. Not for ERC-20 or token balances, contract calls, transaction bodies, receipts, logs, event history or anything that signs or sends. --- # Ethereum Chain Scraper Apify Actor `arman-bd/ethereum-rpc-scraper`. Pick a set of read queries and a network, get one dataset record per query result. It runs without credentials against built-in public nodes; your own node URL is optional and is tried first when supplied. Every method it calls is a read, and no key material exists anywhere in the Actor. ## When to use it - Gas monitoring on a schedule, alerting on `nextBlockBaseFeePerGasGwei`, which is what you actually pay if you send now. - Treasury or wallet balance tracking across several addresses, optionally at `finalized` so the figure cannot be reorged away. - Chain-head watching: detect a stalled network, an unusual block size or a change of proposer, with no indexer in the path. - Cross-chain snapshots of the same address on mainnet, Base and Arbitrum One, one run per network. - Exact wei arithmetic downstream, where the 26-digit balance of a large contract has to survive without rounding. ## When not to use it - Token balances or any contract call. That needs ABI-encoded `eth_call`, which this Actor does not do. Native currency only. - Transaction bodies, receipts, logs or event history. `getBlock` returns the header plus a transaction count, not the transactions. - Anything that writes. No signing, no sending, no key handling. - Deep historical state against the built-in nodes. Public nodes prune, so an old `getBalance` will often be refused unless you point `rpcEndpoint` at an archive node. - Chains other than mainnet, Sepolia, Base and Arbitrum One. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/ethereum-rpc-scraper').call({ queries: ['gasPrice', 'getBalance'], addresses: ['0x00000000219ab540356cBB839Cbe05303d7705Fa'], blockNumbers: ['finalized'], network: 'mainnet', }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); for (const row of items) { // BigInt, not Number: wei is far past float precision. if (row.method === 'eth_getBalance') console.log(row.address, BigInt(row.balanceWei)); } ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~ethereum-rpc-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"queries":["gasPrice","blockNumber"],"network":"mainnet"}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/ethereum-rpc-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `queries` | string[] | yes | `["gasPrice","blockNumber"]` | Any of `gasPrice`, `blockNumber`, `getBalance`, `getBlock`. Deduplicated. An unrecognised entry throws before any read. | | `addresses` | string[] | no | `[]` | `0x` plus 40 hex characters. Required when `getBalance` is selected. A malformed entry throws the whole run rather than being skipped. Ignored by the other queries. | | `blockNumbers` | string[] | no | `["latest"]` | Decimal height, hex height, or one of `latest`, `finalized`, `safe`, `earliest`, `pending`. Used by `getBlock` for which blocks to read and by `getBalance` for the point in history. A malformed entry throws. | | `network` | string | no | `"mainnet"` | One of `mainnet`, `sepolia`, `base`, `arbitrum`. One network per run. | | `rpcEndpoint` | string | no | `""` | Full http(s) URL of your own node. Tried first; the built-in public nodes stay behind it as fallbacks. Anything not starting `http` throws. | **`blockNumbers` doing double duty is the thing that decides row count.** `gasPrice` and `blockNumber` ignore both `addresses` and `blockNumbers` and always produce exactly one row each at the chain head. `getBlock` produces one row per block reference. `getBalance` produces one row per address per block reference, so three addresses against `["latest","finalized"]` is six rows and twelve underlying calls. Keep `blockNumbers` short unless you genuinely want the matrix, and remember that adding a historical height for `getBlock` also silently asks for historical balances that a pruning node will refuse. ## Output One record per query result. The row shape depends on `method`, so branch on it rather than assuming a uniform schema. | Field | Type | Notes | |---|---|---| | `method` | string | `eth_gasPrice`, `eth_blockNumber`, `eth_getBalance` or `eth_getBlockByNumber`. Present on every row and is how you tell the shapes apart. | | `network` | string | Lower-cased network key, as supplied. Every row. | | `chainId` | number | Chain ID the answering node actually reported, not the one implied by `network`. Every row. | | `rpcEndpoint` | string | The node that answered this row. Every row. | | `scrapedAt` | string | Row timestamp, ISO 8601. Every row. | | `blockNumber` | number \| null | Decimal block height. On `gasPrice` and `blockNumber` rows it is the head; on `getBlock` rows it is that block. | | `gasPriceWei` | string \| null | `gasPrice` rows. Exact decimal integer string. | | `gasPriceGwei` | string \| null | `gasPrice` rows. Decimal string produced by integer division, not a float. | | `baseFeePerGasWei` | string \| null | EIP-1559 base fee for the block. Appears on `gasPrice` and `getBlock` rows. | | `baseFeePerGasGwei` | string \| null | Same value in gwei, as a string. | | `nextBlockBaseFeePerGasWei` | string \| null | `gasPrice` rows. The predicted base fee of the **next** block. | | `nextBlockBaseFeePerGasGwei` | string \| null | Same in gwei. This is the number to alert on. | | `maxPriorityFeePerGasWei` | string \| null | `gasPrice` rows. Suggested tip. `null` when the node does not offer the suggestion. | | `maxPriorityFeePerGasGwei` | string \| null | Same in gwei. | | `address` | string | `getBalance` rows. Lower-cased, whatever case you passed. | | `blockTag` | string | `getBalance` and `getBlock` rows. The normalised reference, see the note below about decimal heights. | | `balanceWei` | string \| null | `getBalance` rows. Exact, routinely beyond float range. | | `balanceEth` | string \| null | `getBalance` rows. Decimal string, full precision, no trailing zeros. | | `currency` | string | `getBalance` rows. `ETH` on all four supported networks. | | `nonce` | number \| null | `getBalance` rows. Transaction count at that block. `null` if only the nonce call failed. | | `blockHash` | string \| null | `getBlock` rows. | | `parentHash` | string \| null | `getBlock` rows. | | `timestamp` | string \| null | `getBlock` rows. ISO 8601, derived from the block's Unix seconds. | | `timestampUnix` | number \| null | `getBlock` rows. Raw Unix seconds. | | `transactionCount` | number \| null | `getBlock` rows. Count only; the transactions themselves are not returned. | | `miner` | string \| null | `getBlock` rows. Fee recipient or proposer address. | | `gasUsed` | string \| null | `getBlock` rows. Decimal **string**, not a number. | | `gasLimit` | string \| null | `getBlock` rows. Decimal **string**, not a number. | | `blockSizeBytes` | number \| null | `getBlock` rows. | A `gasPrice` row on mainnet: ```json { "method": "eth_gasPrice", "network": "mainnet", "chainId": 1, "rpcEndpoint": "https://ethereum-rpc.publicnode.com", "blockNumber": 25695686, "gasPriceWei": "192636783", "gasPriceGwei": "0.192636783", "baseFeePerGasWei": "192536783", "baseFeePerGasGwei": "0.192536783", "nextBlockBaseFeePerGasWei": "194809698", "nextBlockBaseFeePerGasGwei": "0.194809698", "maxPriorityFeePerGasWei": "100000", "maxPriorityFeePerGasGwei": "0.0001", "scrapedAt": "2026-08-06T11:43:53.191Z" } ``` A `getBalance` row, with a 26-digit wei value intact: ```json { "method": "eth_getBalance", "network": "mainnet", "chainId": 1, "rpcEndpoint": "https://ethereum-rpc.publicnode.com", "address": "0x00000000219ab540356cbb839cbe05303d7705fa", "blockTag": "latest", "balanceWei": "89299316879836548086007430", "balanceEth": "89299316.87983654808600743", "currency": "ETH", "nonce": 1, "scrapedAt": "2026-08-06T11:43:53.289Z" } ``` A `getBlock` row: ```json { "method": "eth_getBlockByNumber", "network": "mainnet", "chainId": 1, "rpcEndpoint": "https://ethereum-rpc.publicnode.com", "blockTag": "latest", "blockNumber": 25695686, "blockHash": "0x5072bea4d9c5cb8907c67b9a465d0b3e06767e73d5727e3a562c7f6669412fb6", "parentHash": "0x80460ad541a0f9e93a7658e02534b4d2798672406ee818abc0bdf94345ebfa95", "timestamp": "2026-08-06T11:43:47.000Z", "timestampUnix": 1786016627, "transactionCount": 509, "miner": "0x396343362be2a4da1ce0c1c210945346fb82aa49", "gasUsed": "32833224", "gasLimit": "60000000", "baseFeePerGasWei": "192536783", "baseFeePerGasGwei": "0.192536783", "blockSizeBytes": 247593, "scrapedAt": "2026-08-06T11:43:53.346Z" } ``` A `blockNumber` row carries only the five always-present fields plus `blockNumber`. ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "network": "mainnet", "chainId": 1, "rpcEndpoint": "https://ethereum-rpc.publicnode.com", "endpointsUsed": ["https://ethereum-rpc.publicnode.com"], "endpointFailures": [], "queriesRequested": ["gasPrice", "getBalance"], "queriesFailed": 0, "failures": [], "recordsSaved": 3, "filters": { "queries": ["gasPrice", "getBalance"], "addresses": ["0x00000000219ab540356cbb839cbe05303d7705fa"], "blockNumbers": ["latest"], "network": "mainnet", "rpcEndpoint": null }, "finishedAt": "2026-08-06T11:44:59.062Z" } ``` `queriesFailed` above zero means part of what you asked for is missing from the dataset, and each `failures` entry names the `query` (for `getBalance` and `getBlock` this includes the address and block tag) and the `error`. `endpointFailures` is a different thing: it lists nodes rejected during selection or failed over mid-run, each as `endpoint` plus `error`, and a run can be perfectly complete with a non-empty `endpointFailures`. Check `filters.blockNumbers` to see what your block references were normalised to, and `filters.addresses` for the deduplicated, lower-cased list. ## Behaviour to plan around - **A decimal block height comes back as hex.** `18000000` in `blockNumbers` is normalised before use, so the `blockTag` on the resulting rows reads `0x112a880`. Join on `blockNumber`, which is decimal, not on `blockTag`, or normalise your own side the same way. - **Every wei value is a string, on purpose.** Balances exceed `Number.MAX_SAFE_INTEGER` routinely, and the gwei and ETH fields are formatted by integer division rather than floating-point maths. Parse with `BigInt` or a decimal library. `parseFloat` will silently corrupt the low-order digits. `gasUsed` and `gasLimit` are strings too, even though they would fit in a number. - **Bad input throws the whole run, it does not skip.** A malformed address, a malformed block reference, an unknown query name, an unknown network, or `getBalance` with no addresses all fail before the first read, with a message naming the offending value. Validate before you call if you are feeding it a list from elsewhere. - **`chainId` is what the node said, not what you asked for.** If a custom `rpcEndpoint` reports a different chain than `network`, the run logs a warning and continues, because a self-hosted fork is a legitimate target. Compare `chainId` against your expectation yourself. - **Historical balances usually fail on public nodes.** State is pruned, so `getBalance` at an old height is often refused while `getBlock` at the same height works. That lands in `failures` per address and tag, and the rest of the run continues. Use an archive node through `rpcEndpoint` for deep history. - **Node selection and failover are automatic and visible.** Candidates are probed for both chain ID and head height before anything is read, and a node that later rejects an entire batch causes one move to the next candidate. Rows written before and after a failover carry different `rpcEndpoint` values in the same dataset. - **Fields go `null` rather than the row disappearing.** If only the tip or the fee history call fails, the `gasPrice` row is still written with those fields `null`. Test for `null` on every optional field before arithmetic. - **One failed query never aborts the run.** The Actor only throws when nothing at all was saved. `queriesFailed > 0` with a successful run status is the normal partial case. - **`gasPrice` and `blockNumber` are always the chain head.** There is no way to ask for a historical gas price. For that, read `baseFeePerGasWei` off a `getBlock` row at the height you care about. ## Recipes **Gas alerting every few minutes.** One row, one query, cheapest possible run. ```json { "queries": ["gasPrice"], "network": "mainnet" } ``` Alert on `nextBlockBaseFeePerGasGwei` compared as a decimal string, not on `gasPriceGwei`, which is the node's estimate for the current block. **Treasury balances that cannot be reorged away.** Read at `finalized` rather than `latest`. ```json { "queries": ["getBalance"], "addresses": [ "0x00000000219ab540356cBB839Cbe05303d7705Fa", "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" ], "blockNumbers": ["finalized"], "network": "mainnet" } ``` Two rows. Store `balanceWei` as a string or a decimal column, and diff against the previous run keyed on `address` plus `blockTag`. **Chain-liveness watch on an L2.** Head plus two headers is enough to spot a stall or a proposer change. ```json { "queries": ["blockNumber", "getBlock"], "blockNumbers": ["latest", "finalized"], "network": "base" } ``` Compare `timestampUnix` on the `latest` header against your own clock to measure how far behind the chain is. **Through your own node, with the public ones as a safety net.** ```json { "queries": ["getBalance"], "addresses": ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"], "blockNumbers": ["18000000"], "network": "mainnet", "rpcEndpoint": "https://your-archive-node.example/v1/key" } ``` Historical balances need an archive node. Check `RUN_SUMMARY.rpcEndpoint` afterwards to confirm the answer came from yours and not from a fallback.