Data access · 8 min read

Blockchain data API: the four kinds, and how to query one

Four different things get called a blockchain data API, and they are not substitutes for each other. This page separates them in one table, then spends the rest of its length on a working query: one request against a public dataset, the same thing in about 50 lines of dependency-free Python, and what changes when the range grows from one block to a day.

Every command and every output below was run against portal.sqd.dev/datasets/ethereum-mainnet and pasted back unmodified.

Updated 2026-09-04 · By the SQD team

1. The four kinds, and what each is for

The phrase covers four categories that answer different questions. Most confusion about which provider to use comes down to which of these you need.

KindReturnsBuilt for
  • Node and RPC APIsRaw, undecoded data for one chain, one call at a timeSubmitting transactions, reading current state
  • Indexed-data APIsDecoded history you can filter and range overAnalytics, backfills, anything historical
  • Decentralized data networksThe same decoded history, served by independent operatorsMulti-chain history without one vendor in the path
  • Market-data APIsPrices, candles, volumes, usually aggregated offchainPricing and charting, not onchain provenance
The distinction that matters is current state versus history, and one chain versus many.

The rest of this page is about the second and third rows, because that is where the practical work is. An RPC endpoint is easy to reach and well documented; what people actually get stuck on is reading a range of history without making one request per block.

2. One request, one block

Start with the smallest useful thing: every USDC transfer in a single Ethereum block. USDC is 0xa0b8…eb48, and an ERC-20 Transfer has topic0 0xddf252ad…523b3ef. Nothing to install, nothing to sign up for.

your terminal
curl -s https://portal.sqd.dev/datasets/ethereum-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "evm",
"fromBlock": 21000000,
"toBlock": 21000000,
"logs": [{
"address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
"topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
}],
"fields": {
"block": { "number": true, "timestamp": true },
"log": { "address": true, "topics": true, "data": true, "transactionHash": true }
}
}'

One line of newline-delimited JSON comes back, one object per block. Reformatted here with the first log kept intact:

{
"header": { "number": 21000000, "timestamp": 1729345547 },
"logs": [
{
"transactionHash": "0x24e20c506fd16546178a03c955bca381376f97b9ff5aefb726abf84dea6c8913",
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"data": "0x00000000000000000000000000000000000000000000000000000001c119c784",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000d91efec7e42f80156d1d9f660a69847188950747",
"0x0000000000000000000000003974549dc16bf72af6fc3668d5f6c092c9e91c2b"
]
},
...
]
}

Block 21,000,000 was mined at 2024-10-19 13:45:47 UTC. The three topics are the event signature, the sender and the recipient; the amount sits in data as hex, six decimals for USDC, so 0x1c119c784 is 7,534.66 USDC. Note what the request did not need: no ABI, no schema, no deployed indexer, and no separate call to find out which transactions those logs belonged to.

3. The same thing in Python

No web3 library, no SDK, no dependencies. This is the whole program, and it covers a full day rather than one block:

usdc_day.py
import json, urllib.request
URL = "https://portal.sqd.dev/datasets/ethereum-mainnet/stream"
USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
FROM_BLOCK, TO_BLOCK = 21_000_000, 21_007_199
def blocks(from_block, to_block):
"""Yield every matching block, following the stream's continuation."""
cursor = from_block
while cursor <= to_block:
body = {
"type": "evm",
"fromBlock": cursor,
"toBlock": to_block,
"logs": [{"address": [USDC], "topic0": [TRANSFER]}],
"fields": {"block": {"number": True},
"log": {"topics": True, "data": True}},
}
req = urllib.request.Request(
URL, data=json.dumps(body).encode(), method="POST",
headers={"content-type": "application/json",
# Without a User-Agent, Python's default is rejected at
# the edge with a 403 before it reaches Portal.
"user-agent": "usdc-day/1.0"})
last = None
for line in urllib.request.urlopen(req):
line = line.strip()
if not line:
continue
block = json.loads(line)
last = block["header"]["number"]
yield block
# A response covers as much of the range as fits. No last block means
# the range is exhausted; otherwise resume from the one after it.
if last is None or last >= to_block:
return
cursor = last + 1
total = 0
volume = 0
senders = set()
for block in blocks(FROM_BLOCK, TO_BLOCK):
for log in block.get("logs", []):
total += 1
volume += int(log["data"], 16)
senders.add("0x" + log["topics"][1][-40:])
print(f"transfers : {total:,}")
print(f"volume : {volume / 1e6:,.2f} USDC")
print(f"senders : {len(senders):,}")

Output, in about 3 seconds:

transfers : 62,668
volume : 1,739,536,890.04 USDC
senders : 16,626

Two things in that script are worth calling out, because both cost time to discover.

The User-Agent header is load bearing. Without it, Python's default Python-urllib/3.x is rejected at the edge and you get a bare HTTP Error 403: Forbidden with no body, which reads like an authentication problem and is not one. Any identifying string fixes it. curl sets its own, which is why section 2 works unmodified.

The continuation loop is not optional. That is section 4.

4. What happens over a whole day

Ask for blocks 21,000,000 to 21,007,199, which is 7,200 blocks and 24.07 hours at Ethereum's 12.04-second average over that window, and the first response does not contain all of it. It stops at block 21,001,566 and ends there. No error, no truncation flag, no cursor header.

That is the behaviour to design around: a response covers as much of the requested range as fits, and you resume from the last block it returned. Take the last header.number, add one, ask again. Loop until you reach your toBlock. Code that ignores this looks like it works and silently reports a fraction of the answer, which is the worst failure mode available.

MeasureValue
  • Blocks requested7,200
  • Block objects returned7,068
  • Blocks with a USDC transfer7,064
  • Transfer logs returned62,668
  • HTTP requests needed5
  • Blocks per response1,567 / 1,796 / 1,761 / 1,732 / 344
  • Downloaded18.8 MB
  • Wall clock3.2 s
Every USDC transfer over 7,200 Ethereum blocks, measured 2026-09-04.

The chunk sizes are the tell: they are not a round number of blocks, and they vary. The cut is made on response size, not block count, so a range over a busy contract returns fewer blocks per response than a quiet one. Do not hard-code a page size. Four of the 7,068 block objects carry no transfer at all: each response ends on the last block it covered, matched or not, so the client always knows where to resume.

Five requests for a day of one contract's transfers is the number to compare against whatever you are using now. The equivalent over JSON-RPC is bounded by two things this is not: eth_getLogs carries provider-imposed caps on block range and result count, so the loop is driven by those limits rather than by payload size, and whether a range this old is served at all depends on the provider's history retention. Logs come from receipts, which a full node keeps unless it prunes them; archive mode is about historical state, not old logs. Neither is a criticism of RPC; it is a different tool, built for the first row of the table in section 1.

5. Which kind answers which question

Match the category to the question before comparing providers inside it.

If the question isReach for
  • What is this account's balance right nowNode or RPC API
  • Send this transactionNode or RPC API
  • Every transfer of this token last monthIndexed-data API
  • Rebuild this protocol's history from genesisIndexed-data API
  • The same query across twenty chainsDecentralized data network
  • What was ETH worth on TuesdayMarket-data API

The queries on this page are the third and fourth rows. If you want to see the same request shape run against a chain other than Ethereum, one query shape, every VM covers the cross-chain case, and RPC vs indexed data goes further into the first two rows.

Frequently asked questions

What is a blockchain data API?
Any programmatic interface that returns data about a blockchain's state or history. The term covers four categories that solve different problems: node and RPC APIs (raw current-state access over JSON-RPC), indexed-data APIs (decoded, queryable history), decentralized data networks (multi-chain data served by independent operators), and market-data APIs (prices and candles, usually aggregated offchain). Picking one starts with working out which category your question belongs to, because they are not substitutes for each other.
How do I query blockchain data in Python?
You do not need a web3 library to read indexed data. The example in this guide is about 50 lines of standard-library Python: build a JSON body describing the contract address and event topic you want, POST it, and read the newline-delimited JSON that comes back. One thing to watch: Python's default User-Agent is rejected at the edge with a 403, so set a User-Agent header. The script returns 62,668 USDC transfers over a 24-hour range in about 3 seconds.
What is the difference between an RPC and a blockchain data API?
An RPC endpoint is one kind of blockchain data API: the request-response interface a node exposes through methods like eth_getBlockByNumber and eth_getLogs. It returns raw, undecoded data for a single chain and is built for current-state lookups and submitting transactions. "Blockchain data API" is the broader umbrella that also covers indexed-data APIs and decentralized data networks, which return decoded history and support range queries that JSON-RPC was never designed for.
Why does my request return 403?
Almost always a missing or default User-Agent header. Python's urllib sends "Python-urllib/3.x" by default and it is rejected at the edge before the request reaches Portal, so you get a 403 with no body rather than an API error. Set any identifying User-Agent string and the same request succeeds. curl sets its own User-Agent, which is why the curl examples work unmodified.
Can I get blockchain data without running a node?
Yes. Hosted RPC providers, indexed-data APIs, and decentralized data networks all serve blockchain data without you operating anything. Running your own archive node means multiple terabytes per chain plus ongoing operations, so most teams read through a provider and reserve self-hosting for cases with specific control or compliance requirements.
What is the best blockchain data API?
There is no single best one, because the four categories answer different questions. RPC is the right answer for submitting transactions and reading current state. Indexed-data APIs are the right answer for decoded history and aggregation over ranges. Decentralized data networks are the right answer for multi-chain history without a single vendor in the path. Market-data APIs are the right answer for prices. Match the category to your dominant access pattern first, then compare providers on coverage.

Run the query yourself

ethereum-mainnet is one of the public datasets on Portal. Change the address and the topic and the same request works.