Documentation Index Fetch the complete documentation index at: https://docs.sqd.dev/llms.txt
Use this file to discover all available pages before exploring further.
This page provides a quick reference for the Portal API. For the complete OpenAPI specification with all fields and options:
Complete EVM API Specification View the full OpenAPI documentation with all available fields, filters, and
detailed schemas →
Base URL
All Portal requests are made to:
https://portal.sqd.dev/datasets/{network}
Replace {network} with your target blockchain (e.g., ethereum-mainnet, polygon-mainnet, arbitrum-one).
Core Endpoints
POST /stream Stream blockchain data with filters. Includes unfinalized blocks.
POST /finalized-stream Stream only finalized blocks. Use for production pipelines requiring
certainty.
GET /metadata Get dataset information: block range, finality rules, and network details.
Installation
npm install @subsquid/portal-client
Quick Start Examples
curl --compressed -X POST 'https://portal.sqd.dev/datasets/ethereum-mainnet/stream' \
-H 'Content-Type: application/json' \
-d '{
"type": "evm",
"fromBlock": 18000000,
"toBlock": 18001000,
"fields": {
"block": { "number": true, "timestamp": true },
"log": { "address": true, "topics": true, "data": true }
}
}'
Available Fields
Block Fields
Select which block header fields to retrieve:
fields : {
block : {
number : true , // Block number
hash : true , // Block hash
parentHash : true , // Parent block hash
timestamp : true , // Unix timestamp
size : true , // Block size in bytes
gasUsed : true , // Total gas used
gasLimit : true , // Gas limit
baseFeePerGas : true , // Base fee (EIP-1559)
stateRoot : true , // State root hash
transactionsRoot : true , // Transactions root
receiptsRoot : true , // Receipts root
logsBloom : true , // Logs bloom filter
difficulty : true , // Difficulty
totalDifficulty : true , // Total difficulty
miner : true , // Block producer address
nonce : true , // Block nonce
mixHash : true , // Mix hash
extraData : true , // Extra data
}
}
Transaction Fields
Select transaction data fields:
fields : {
transaction : {
hash : true , // Transaction hash
from : true , // Sender address
to : true , // Recipient address (null for deployments)
value : true , // ETH value transferred
input : true , // Transaction input data
nonce : true , // Sender nonce
gas : true , // Gas limit
gasPrice : true , // Gas price (legacy)
maxFeePerGas : true , // Max fee (EIP-1559)
maxPriorityFeePerGas : true , // Priority fee (EIP-1559)
gasUsed : true , // Actual gas used
cumulativeGasUsed : true , // Cumulative gas in block
effectiveGasPrice : true , // Effective gas price paid
status : true , // Status (1 = success, 0 = failure)
type : true , // Transaction type
chainId : true , // Chain ID
v : true , // Signature V
r : true , // Signature R
s : true , // Signature S
yParity : true , // Y parity (EIP-2930/1559)
contractAddress : true , // Deployed contract address
transactionIndex : true , // Index in block
}
}
Log Fields
Select event log fields:
fields : {
log : {
address : true , // Contract address
topics : true , // Event topics (topic0-topic3)
data : true , // Event data
logIndex : true , // Log index in block
transactionIndex : true , // Transaction index
transactionHash : true , // Transaction hash
}
}
Trace Fields
Select call trace fields:
fields : {
trace : {
type : true , // Trace type (call, delegatecall, etc.)
from : true , // Caller address
to : true , // Callee address
value : true , // Value transferred
input : true , // Call input data
output : true , // Call output data
gas : true , // Gas provided
gasUsed : true , // Gas actually used
error : true , // Error message (if failed)
revertReason : true , // Revert reason (if reverted)
createResultAddress : true , // Created contract address
createResultCode : true , // Created contract code
}
}
State Diff Fields
Select state change fields:
fields : {
stateDiff : {
address : true , // Contract address
key : true , // Storage key
kind : true , // Change type (=, +, *, -)
prev : true , // Previous value
next : true , // New value
}
}
Filtering Options
Log Filters
Filter logs by contract address and event topics:
logs : [
{
address: [ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" ], // USDC
topic0: [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ,
], // Transfer
topic1: [ "0x..." ], // Optional: first indexed param
topic2: [ "0x..." ], // Optional: second indexed param
topic3: [ "0x..." ], // Optional: third indexed param
},
// Multiple filter objects = OR logic
{
address: [ "0x..." ],
topic0: [ "0x..." ],
},
];
Filter logic:
Multiple addresses in one object = OR
Multiple topics in one object = AND
Multiple filter objects = OR
Transaction Filters
Filter transactions by sender, recipient, or function signature:
transactions : [
{
from: [ "0x..." ], // Sender addresses
to: [ "0x..." ], // Recipient addresses
sighash: [ "0xa9059cbb" ], // Function signature (first 4 bytes)
},
];
Trace Filters
Filter traces by type, caller, or callee:
traces : [
{
type: [ "call" , "delegatecall" , "staticcall" ],
callFrom: [ "0x..." ], // Caller address
callTo: [ "0x..." ], // Callee address
callSighash: [ "0x..." ], // Function signature
},
];
State Diff Filters
Filter state changes by contract address:
stateDiffs : [
{
address: [ "0x..." ], // Contract addresses
},
];
Network Selection
Specify the EVM network when creating the data source:
// Ethereum mainnet
const eth = new DataSource ({ network: "ethereum-mainnet" });
// Ethereum testnet
const sepolia = new DataSource ({ network: "ethereum-sepolia" });
// Layer 2 networks
const arbitrum = new DataSource ({ network: "arbitrum-one" });
const optimism = new DataSource ({ network: "optimism-mainnet" });
const base = new DataSource ({ network: "base-mainnet" });
const polygon = new DataSource ({ network: "polygon-mainnet" });
// Other EVM chains
const bsc = new DataSource ({ network: "binance-mainnet" });
const avalanche = new DataSource ({ network: "avalanche-mainnet" });
View All Networks See complete list of 100+ supported EVM networks
Query Patterns
Pattern 1: Range Query
Query a specific block range:
const blocks = await dataSource . getBlocks ({
from: 18000000 ,
to: 18100000 ,
fields: {
/* fields */
},
logs: [
/* filters */
],
});
Pattern 2: Batch Processing
Process large ranges in batches:
const BATCH_SIZE = 10000 ;
for ( let i = startBlock ; i < endBlock ; i += BATCH_SIZE ) {
const blocks = await dataSource . getBlocks ({
from: i ,
to: Math . min ( i + BATCH_SIZE , endBlock ),
fields: {
/* fields */
},
});
// Process blocks
await processBlocks ( blocks );
}
Pattern 3: Multi-Contract Query
Query multiple contracts simultaneously:
const blocks = await dataSource . getBlocks ({
from: 18000000 ,
to: 18100000 ,
fields: {
log: { address: true , topics: true , data: true },
},
logs: [
{
address: [
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" , // USDC
"0xdAC17F958D2ee523a2206206994597C13D831ec7" , // USDT
"0x6B175474E89094C44Da98b954EedeAC495271d0F" , // DAI
],
topic0: [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ,
],
},
],
});
1. Request Only Needed Fields
// Good: Minimal fields
fields : {
log : {
address : true ,
topics : true ,
}
}
// Avoid: Requesting unnecessary fields
fields : {
log : {
address : true ,
topics : true ,
data : true , // Only if needed
transactionHash : true , // Only if needed
blockNumber : true , // Only if needed
}
}
2. Use Specific Filters
// Good: Filter at Portal level
logs : [
{
address: [ "0xSpecificContract" ],
topic0: [ "0xSpecificEvent" ],
},
];
// Avoid: Fetching everything and filtering in code
logs : []; // Gets all logs
3. Optimal Batch Sizes
// Good: 10k-50k blocks per query for most use cases
const BATCH_SIZE = 10000 ;
// Adjust based on:
// - More filters = larger batches OK
// - More fields = smaller batches
// - More activity (mainnet) = smaller batches
4. Parallel Queries
// Process multiple ranges in parallel
const ranges = [
{ from: 18000000 , to: 18100000 },
{ from: 18100000 , to: 18200000 },
{ from: 18200000 , to: 18300000 },
];
const results = await Promise . all (
ranges . map (( range ) =>
dataSource . getBlocks ({
from: range . from ,
to: range . to ,
fields: {
/* fields */
},
})
)
);
Stream continuation
A single /stream response is a batch , not the complete range. The server may close the connection at any point: when a worker’s range ends, when a connection is recycled, or at the current dataset head. Clients that treat one HTTP response as the whole query will silently drop blocks.
To stream a range to completion, loop:
async function streamRange ( from : number , to : number | undefined , body : any ) {
let currentFrom = from ;
while ( to === undefined || currentFrom <= to ) {
const res = await fetch (
"https://portal.sqd.dev/datasets/ethereum-mainnet/stream" ,
{
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ ... body , fromBlock: currentFrom , toBlock: to }),
}
);
if ( res . status === 204 ) break ; // range is above the dataset head
if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } : ${ await res . text () } ` );
const lines = ( await res . text ()). trim (). split ( " \n " ). filter ( Boolean );
if ( lines . length === 0 ) break ;
let lastBlock = currentFrom - 1 ;
for ( const line of lines ) {
const block = JSON . parse ( line );
// ... process block ...
lastBlock = block . header . number ;
}
currentFrom = lastBlock + 1 ;
}
}
Key points:
Read the last block’s header.number, issue the next request with fromBlock = lastNumber + 1.
Keep toBlock, fields, and filters identical across retries.
A 204 No Content response means the range is above the dataset head. For historical queries, you’re done; for open-ended queries, wait and retry.
Handling reorgs near the chain head
When a request starts at or near the chain tip, the block you asked for may no longer be on the canonical chain by the time Portal processes the query. To detect this, pass parentBlockHash with each request (the hash of the block immediately before fromBlock):
const body = {
type: "evm" ,
fromBlock: lastBlock . number + 1 ,
parentBlockHash: lastBlock . hash , // hash of lastBlock, i.e. parent of fromBlock
fields: { block: { number: true , hash: true , parentHash: true } },
logs: [
/* filters */
],
};
If the chain has reorged away from parentBlockHash, Portal returns HTTP 409 with a list of blocks that are on the current canonical chain:
{
"previousBlocks" : [
{ "number" : 24928291 , "hash" : "0x129bc9..." },
{ "number" : 24928290 , "hash" : "0x8af2be..." }
]
}
Client recovery:
Find a block in your local state whose (number, hash) matches one of previousBlocks.
Roll back your pipeline to that block.
Resume the stream with fromBlock = matched.number + 1 and parentBlockHash = matched.hash.
Streams opened against /finalized-stream (instead of /stream) never return 409: only finalized blocks are served, so there’s nothing to reorg. Use that endpoint when you don’t need real-time data.
Error Handling
try {
const blocks = await dataSource . getBlocks ({
from: 18000000 ,
to: 18100000 ,
fields: {
/* fields */
},
});
} catch ( error ) {
if ( error . code === "RATE_LIMIT_EXCEEDED" ) {
// HTTP 429: Public Portal rate limit. Honor Retry-After if set.
console . error ( "Rate limit exceeded, waiting..." );
await new Promise (( resolve ) => setTimeout ( resolve , 10000 ));
} else if ( error . code === "CONFLICT" ) {
// HTTP 409: chain reorg. See "Handling reorgs near the chain head" above.
const { previousBlocks } = error . body ;
// Roll back to a common ancestor and resume.
} else if ( error . code === "INVALID_BLOCK_RANGE" ) {
// HTTP 400: fromBlock is below the dataset's start_block, or the query is malformed.
console . error ( "Invalid block range" );
} else if ( error . code === "NETWORK_ERROR" ) {
// Transport error, retry with backoff.
console . error ( "Network error, retrying..." );
} else {
// HTTP 500: do not retry. HTTP 503: retry later, honor Retry-After.
console . error ( "Unknown error:" , error );
}
}
Common Use Cases
Use Case 1: Event Monitoring
Monitor specific smart contract events:
const blocks = await dataSource . getBlocks ({
from: latestProcessedBlock + 1 ,
fields: {
block: { number: true , timestamp: true },
log: { address: true , topics: true , data: true },
},
logs: [
{
address: [ CONTRACT_ADDRESS ],
topic0: [ EVENT_SIGNATURE ],
},
],
});
Use Case 2: Transaction Tracking
Track transactions to/from specific addresses:
const blocks = await dataSource . getBlocks ({
from: startBlock ,
to: endBlock ,
fields: {
transaction: { hash: true , from: true , to: true , value: true },
},
transactions: [
{
from: [ WATCHED_ADDRESS ],
},
],
});
Use Case 3: Analytics Pipeline
Build analytics on blockchain data:
const blocks = await dataSource . getBlocks ({
from: startBlock ,
to: endBlock ,
fields: {
block: { number: true , timestamp: true , gasUsed: true },
transaction: { gasUsed: true , value: true },
},
});
// Aggregate statistics
const stats = blocks . map (( block ) => ({
block: block . header . number ,
txCount: block . transactions . length ,
totalGas: block . header . gasUsed ,
totalValue: block . transactions . reduce (
( sum , tx ) => sum + BigInt ( tx . value ),
0 n
),
}));
Request Structure Reference
Every Portal request follows this structure:
{
type : "evm" , // Chain type
fromBlock : number , // Starting block (inclusive)
toBlock ?: number , // Ending block (inclusive, optional)
fields : { // What data to return
block ?: { ... },
transaction ?: { ... },
log ?: { ... },
trace ?: { ... },
stateDiff ?: { ... }
},
logs ?: [ ... ], // Filter by event logs
transactions ?: [ ... ], // Filter by transactions
traces ?: [ ... ], // Filter by call traces
stateDiffs ?: [ ... ] // Filter by state changes
}
Portal returns newline-delimited JSON (NDJSON) , with one block per line:
{ "header" : { "number" : 18000000 , ... }, "logs" : [ ... ], "transactions" : [ ... ]}
{ "header" : { "number" : 18000001 , ... }, "logs" : [ ... ], "transactions" : [ ... ]}
{ "header" : { "number" : 18000002 , ... }, "logs" : [ ... ], "transactions" : [ ... ]}
This format enables constant-memory streaming of arbitrary ranges.
Common Patterns
Process large block ranges by splitting into manageable chunks: const BATCH_SIZE = 10000 ;
for ( let from = startBlock ; from < endBlock ; from += BATCH_SIZE ) {
const to = Math . min ( from + BATCH_SIZE , endBlock );
const blocks = await fetch ( /* ... */ );
}
Stream new blocks as they arrive using /finalized-stream: const response = await fetch (
'https://portal.sqd.dev/datasets/ethereum-mainnet/finalized-stream' ,
{ method: 'POST' , body: JSON . stringify ({
type: 'evm' ,
fromBlock: latestProcessedBlock + 1 ,
fields: { /* ... */ }
})}
);
Handle rate limits and network errors gracefully: try {
const response = await fetch ( /* ... */ );
if ( ! response . ok ) {
if ( response . status === 429 ) {
// Rate limited - wait and retry
await sleep ( 10000 );
}
}
} catch ( error ) {
// Network error - implement retry logic
}
Complete API Specification
For the complete OpenAPI specification with all available fields, options, and detailed schemas:
Full EVM API Specification View complete OpenAPI documentation with all endpoints, fields, and options →
Next Steps
View Examples Explore practical Portal examples for common use cases
Supported Networks See complete list of 100+ supported EVM networks
Quickstart Guide Make your first Portal request in 5 minutes