Moonbeam API: Query Account Balances Without Scanning Every Block
Use a Moonbeam RPC endpoint to confirm connectivity, then query indexed balance data with SubQuery instead of walking blocks by hand. Includes a runnable Node.js 18+ example, expected output, and failure handling.
If you need an account's balance history on Moonbeam, the fastest path is usually not a raw RPC loop. RPC gives you point-in-time state and lets you submit calls; an indexer gives you queryable, historical, pre-processed data. This guide shows the split in practice: a small RPC health check against a Moonbeam endpoint, then a SubQuery GraphQL query for indexed data. You will end with a runnable Node.js 18+ script and a clear idea of which tool answers which question.
RPC access is not indexing
A Moonbeam RPC endpoint answers questions about current or recent chain state: eth_chainId, eth_getBalance, eth_getBlockByNumber, eth_call, and similar. It is the right tool when you need one value now, or when you are submitting a transaction. It is not a database. If you want every transfer for an address over the last 30 days, RPC alone means paging blocks and filtering logs yourself, and you own the retry, reorg, and storage logic.
An indexer ingests blocks once and exposes the result as a query API, usually GraphQL. SubQuery is one such indexing framework, and Moonbeam has public SubQuery projects you can query. The distinction matters for correctness: an RPC balance is authoritative for a block, while an indexed balance is derived from processed events and must be validated against the chain when precision matters.
A practical workflow is to keep an RPC endpoint for live reads and transaction submission, and use an indexer for list and history queries. If you are choosing an endpoint, the Moonbeam network page on OnFinality lists the network and its API options; the RPC Assistant helps you assemble a request for a specific method.
- RPC: single-value reads, current state, transaction submission
- Indexer: historical lists, aggregations, filtered event queries
- Validate indexed numbers against an RPC read before using them in accounting logic
Step 1: confirm your Moonbeam RPC endpoint responds
Before writing any query logic, confirm the endpoint is reachable and returning the chain you expect. Moonbeam mainnet uses chain ID 128. A mismatch here is the most common cause of confusing downstream failures, so check it first.
The script below uses only the built-in fetch available in Node.js 18 and later, so there is nothing to install. It sends a JSON-RPC eth_chainId request, checks the HTTP status, checks for a JSON-RPC error object, and asserts the returned chain ID. Replace the placeholder URL with the endpoint you obtained from your provider; do not hardcode an endpoint you have not verified.
// check-moonbeam.mjs — Node.js 18+
const RPC_URL = process.env.MOONBEAM_RPC_URL;
const EXPECTED_CHAIN_ID = 128n; // Moonbeam mainnet
if (!RPC_URL) {
console.error('Set MOONBEAM_RPC_URL to your Moonbeam endpoint first.');
process.exit(1);
}
async function rpc(method, params = []) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} from RPC endpoint`);
}
const body = await res.json();
if (body.error) {
throw new Error(`JSON-RPC error ${body.error.code}: ${body.error.message}`);
}
return body.result;
}
try {
const chainId = BigInt(await rpc('eth_chainId'));
if (chainId !== EXPECTED_CHAIN_ID) {
throw new Error(`Wrong chain: got ${chainId}, expected ${EXPECTED_CHAIN_ID}`);
}
console.log('Connected to Moonbeam mainnet, chainId =', chainId.toString());
} catch (err) {
console.error('RPC check failed:', err.message);
process.exit(1);
}
Expected result: Connected to Moonbeam mainnet, chainId = 128.
Failure handling is deliberate. A non-2xx response throws with the status code, a JSON-RPC error object throws with its code and message, and a 10-second timeout aborts the request so a hung connection does not stall your process. A wrong chain ID is treated as a hard failure rather than a warning, because every later read would be against the wrong network.
If you see a timeout, check that the URL is an HTTP(S) endpoint and not a WebSocket URL. If you see Wrong chain, you are probably pointed at a testnet such as Moonbase Alpha, which uses a different chain ID.
Step 2: query indexed data with SubQuery
Once the RPC layer is confirmed, switch to the query layer for anything list-shaped. SubQuery exposes a GraphQL endpoint per project. The example below queries a public Moonbeam SubQuery project for recent transfers involving an address. Field names differ between projects, so open the project's GraphQL playground and confirm the schema before adapting this code.
The important habit is to treat the GraphQL endpoint as a separate dependency from the RPC endpoint. They fail differently: GraphQL returns a 200 with an errors array for query problems, while RPC returns a JSON-RPC error object. Handle both.
// query-transfers.mjs — Node.js 18+
const GRAPHQL_URL = process.env.SUBQUERY_GRAPHQL_URL;
const ADDRESS = process.env.MOONBEAM_ADDRESS;
if (!GRAPHQL_URL || !ADDRESS) {
console.error('Set SUBQUERY_GRAPHQL_URL and MOONBEAM_ADDRESS first.');
process.exit(1);
}
const query = `
query RecentTransfers($addr: String!) {
transfers(
filter: { or: [{ from: { equalTo: $addr } }, { to: { equalTo: $addr } }] }
orderBy: BLOCK_NUMBER_DESC
first: 5
) {
nodes {
id
from
to
amount
blockNumber
}
}
}
`;
async function run() {
const res = await fetch(GRAPHQL_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ query, variables: { addr: ADDRESS } }),
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} from GraphQL endpoint`);
}
const body = await res.json();
if (body.errors?.length) {
throw new Error(`GraphQL error: ${body.errors[0].message}`);
}
const nodes = body.data?.transfers?.nodes ?? [];
if (nodes.length === 0) {
console.log('No indexed transfers found for', ADDRESS);
return;
}
for (const t of nodes) {
console.log(`block ${t.blockNumber}: ${t.from} -> ${t.to} amount ${t.amount}`);
}
}
run().catch((err) => {
console.error('Query failed:', err.message);
process.exit(1);
});
Expected result: up to five lines, each showing a block number, sender, recipient, and amount, newest first. If the address has no indexed activity, the script prints a clear empty-result message instead of failing silently.
The errors array check matters because GraphQL servers commonly return HTTP 200 even when the query is invalid. Without that check you would read body.data as undefined and misdiagnose a schema mismatch as missing data.
If you get a schema error, the field names in your project differ from this example. Open the project's playground, inspect the Query type, and adjust transfers, filter, and orderBy accordingly.
Step 3: reconcile the two sources
Indexed data can lag or diverge from chain state, especially around reorgs. For any number that affects money, read the balance from RPC at a specific block and compare it with what your indexer reports for the same block. If they disagree, trust the RPC value and investigate the indexer's progress.
You can measure freshness yourself rather than relying on a claim: query the indexer for the highest indexed block number, then call eth_blockNumber on your RPC endpoint, and subtract. Run that check on a schedule and log the gap. That gives you a real number for your own setup instead of a borrowed benchmark.
For more patterns like this, the OnFinality Learn library covers RPC and indexing workflows across supported networks.
- Compare indexed and RPC values at the same block height
- Track indexer lag by diffing highest indexed block against
eth_blockNumber - Log the gap over time; do not assume a fixed freshness window
Next step
If you want to build the RPC request for a specific Moonbeam method without hand-writing JSON, use the OnFinality RPC Assistant to generate the payload, then paste it into the script above. For network details and endpoint options, start from the Moonbeam network page.
Keep the two layers separate in your codebase: one module for RPC reads and writes, one for GraphQL queries. That separation makes failures obvious and keeps your indexing assumptions testable.
- Generate RPC payloads with the RPC Assistant
- Confirm network and endpoint details on the Moonbeam network page
- Keep RPC and GraphQL clients in separate modules