Data access · 9 min read

One query shape, every VM: declarative cross-chain data

Most ways of getting onchain data make you commit before you see a single row: define a schema, deploy an indexer, wait for it to sync. SQD's Portal inverts that. You POST one JSON-shaped request, network plus table plus filters plus a block window, and the matching raw rows stream back. The same shape reads EVM logs, Solana instructions, and Bitcoin inputs and outputs against any contract on any supported chain, immediately. This guide shows three real query bodies and the one field that trips people up. Every result below is from a live Portal query.

Updated 2026-06-18 · By the SQD team

1. The shape: network, table, filters, window

Every Portal request is a POST to /datasets/{network}/stream with a JSON body. The body always has the same four parts: a type for the virtual machine, a fromBlock and toBlock window, one or more filter objects named after the table you want, and a fields selection. The response streams back as newline-delimited JSON, one object per line. That envelope does not change between chains.

Same envelope, three data models
EVM
  • typeevm
  • tablelogs
  • filteraddress + topic0
  • windowfromBlock, toBlock
Solana
  • typesolana
  • tableinstructions
  • filterprogramId + discriminator
  • windowfromBlock, toBlock
Bitcoin
  • typebitcoin
  • tableoutputs
  • filterscriptPubKeyAddress
  • windowfromBlock, toBlock
Only the table name and the filter keys change with the chain's data model. The request envelope and the streamed response shape stay the same.

2. EVM: logs by address and topic

On an EVM chain the useful table is usually logs, filtered by a contract address and a topic0 event signature. This reads USDC Transfer events on Base. Scope it to one contract over a tight range: a single stream response is bounded, so an over-broad scan does not fail loudly, it returns a partial window that stops early at a block boundary, and you continue from the last block received.

your terminal
curl -s -X POST https://portal.sqd.dev/datasets/base-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "evm",
"fromBlock": 28000000, "toBlock": 28000100,
"logs": [{
"address": ["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"],
"topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
}],
"fields": { "log": { "address": true, "topics": true, "data": true, "transactionHash": true } }
}'

The first row the query returns is a raw log, exactly as the chain stores it: topics[0] is the Transfer signature, topics[1] and topics[2] are the indexed from and to (each left-padded to 32 bytes), and data is the value:

{
"address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000802b65b5d9016621e66003aed0b16615093f328b",
"0x000000000000000000000000f8f0c4e9df216123d54e8f2194e5941f1b9764d5"
],
"data": "0x00000000000000000000000000000000000000000000000000000000013b6018"
}

Decoded against the Transfer signature, that is a transfer of 20.66844 USDC (data 0x013b6018 = 20668440, with 6 decimals) in transaction 0x22e7…2378.

3. Solana: instructions by program

Solana has no indexed event logs in the EVM sense; its logs table holds free-text program log lines, not topic-keyed events you filter by address and signature. The unit of activity is the instruction, identified by a programId and an Anchor discriminator. Same envelope, different table and filter: this reads Jupiter's sharedAccountsRoute instructions, where the 8-byte discriminator d8 selects the instruction kind.

your terminal
curl -s -X POST https://portal.sqd.dev/datasets/solana-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "solana",
"fromBlock": 427287355, "toBlock": 427287360,
"instructions": [{
"programId": ["JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"],
"d8": ["0xc1209b3341d69c81"]
}],
"fields": { "instruction": { "programId": true, "accounts": true, "data": true, "instructionAddress": true, "error": true, "isCommitted": true } }
}'

A committed match comes back with its place in the call tree and the full ordered account list:

instructionAddress [3]
isCommitted true
error null
accounts [
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA (Token program)
...
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (USDC mint)
So11111111111111111111111111111111111111112 (wrapped SOL)
]

The instructionAddress: [3] places it in the transaction's call tree (top-level, inner, and so on); the ordered accounts list begins with the Token program and includes the mints in the route. The same record also exposes whether the instruction succeeded, which is the subject of the Solana decoding guide.

4. Bitcoin: inputs and outputs

Bitcoin has no contracts and no logs. Value moves through transaction inputs and outputs, and those are the tables. The same request shape attaches them inline:

your terminal
curl -s -X POST https://portal.sqd.dev/datasets/bitcoin-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "bitcoin",
"fromBlock": 954236, "toBlock": 954236,
"transactions": [{}], "inputs": [{}], "outputs": [{}],
"fields": {
"transaction": { "transactionIndex": true, "txid": true },
"input": { "transactionIndex": true, "prevoutScriptPubKeyAddress": true, "prevoutValue": true },
"output": { "transactionIndex": true, "scriptPubKeyAddress": true, "value": true, "scriptPubKeyType": true }
}
}'

Bitcoin uses Bitcoin Core's field names: an input's sender is prevoutScriptPubKeyAddress, an output's recipient is scriptPubKeyAddress. A real transaction (a89a5662…3f3d30) from block 954,236 spends one input into a payment and the change, each output tagged by its script type:

RoleAddressBTCScript type
  • inputbc1qxp3gemd4xe0s4d7kxe3dt8t7ndwp5yg7kl63460.01640293
  • outputbc1q6jd3mvuy7wgdy402t3wg3k6cs62dteunc80w5q0.00741561witness_v0_keyhash
  • outputbc1qxp3gemd4xe0s4d7kxe3dt8t7ndwp5yg7kl63460.00895269witness_v0_keyhash (change)
tx a89a5662…3f3d30

Following value through these inputs and outputs is the whole of Bitcoin flow analysis, covered in the Bitcoin UTXO guide.

5. The type-field trap

The one thing that does change, and the one place a cross-chain query goes wrong, is the type field and the table name. They have to match the dataset's virtual machine. Two cases catch people out: HyperEVM is an EVM chain, while Hyperliquid fills are their own machine on a separate dataset. Check a dataset's virtual machine and tables before you query it.

DatasettypeTable
  • ethereum-mainnet, base-mainnet evm logs, transactions, traces, state_diffs
  • hyperliquid-mainnet (HyperEVM) evm logs, transactions (not fills)
  • hyperliquid-fills hyperliquidFills fills
  • solana-mainnet solana instructions, transactions, balances, token_balances, rewards, logs
  • bitcoin-mainnet bitcoin inputs, outputs (no logs, no contracts)
The discovery call portal_list_networks returns the virtual machine and table set for every dataset, which is the reliable way to pick the right type.

6. Why this is harder elsewhere

An indexer framework asks you to author a schema, deploy it, and wait for a sync before any data exists to query, and that work is per project and per chain. The declarative model returns raw rows on any contract immediately, in the same shape across virtual machines.

The hard part a provider has to solve to offer this is normalizing very different virtual machines, an account-and-log model, an instruction model, and a UTXO model, into one declarative interface without flattening away what makes each one useful. That normalization is the work; the uniform request is the result. Getting the three rows above anywhere else usually means three integrations: an EVM indexer or archive RPC for the log, a Solana-specific decoder for the instruction, and a Bitcoin or UTXO API for the output, each with its own query language. Here it is the same POST three times.

For the same idea applied to one asset across chains, see stablecoin data, which adds time alignment across chains. For the architecture behind landing many chains in one place, see multi-chain indexing. To run these queries from an agent rather than by hand, start with the Portal MCP setup.

Frequently asked questions

What does a declarative blockchain query mean?
A declarative query describes what data you want, not how to fetch it. With SQD's Portal you POST a JSON body that names a network, a table (logs, instructions, inputs, outputs), a set of filters, and a block window. The Portal returns the matching raw rows. You do not write a fetch loop, define a schema, deploy an indexer, or wait for a sync. The same request shape works on any supported contract immediately.
Is the query shape really the same across EVM, Solana, and Bitcoin?
The envelope is the same: a type, a fromBlock and toBlock window, one or more filter objects, and a fields selection. What changes is the table name and the filter keys, because the chains have different data models. EVM logs filter on address and topic0. Solana instructions filter on programId and an Anchor discriminator. Bitcoin filters on inputs and outputs. The structure of the request, and of the streamed response, stays the same.
What is the type-field trap when querying multiple chains?
The type field has to match the dataset's virtual machine, and two cases catch people out. HyperEVM (the hyperliquid-mainnet dataset) is an EVM chain, so it uses type evm and queries logs and transactions. The separate hyperliquid-fills dataset is its own virtual machine and uses type hyperliquidFills with a fills table. Bitcoin has no logs and no contracts, only inputs and outputs. Solana uses instructions, and Substrate uses calls and events.
Why is querying any contract immediately different from using a subgraph?
A subgraph-style indexer needs a schema authored, an indexer deployed, and a sync completed before any data is queryable, and that pipeline is per project. The Portal already indexes the raw data, so you point a query at any contract on any supported chain and get rows back without building or deploying anything first. You add a deployed indexer later if you want your own decoded tables, but you do not need one to read the chain.
How do I align a query window across chains that produce blocks at different rates?
Block numbers are not comparable across chains, so you resolve a timestamp to a block on each chain before querying. The Portal exposes a timestamp-to-block lookup per dataset, which returns the first block at or after an instant. The stablecoin data guide works through a time-aligned cross-chain comparison in detail.

Querying several chains at once?

See how one query model feeds analytics across chains on the analytics solution page.