HyperSync Complete Documentation
This document contains all HyperSync documentation consolidated into a single file for LLM consumption.
| What it is | A purpose-built, high-performance blockchain data retrieval layer built in Rust - a direct alternative to traditional JSON-RPC endpoints |
| Performance | Up to 2000x faster than traditional RPC (e.g. scan Arbitrum for sparse log data in 2 seconds vs. hours/days); ~500x faster for event queries |
| Supported networks | 70+ EVM chains and Fuel, with new networks added regularly |
| Client libraries | Python, Rust, Node.js, Go |
| API token | Required - set via ENVIO_API_TOKEN environment variable |
| Data types | Logs, transactions, traces, blocks - with fine-grained field selection |
| Query features | Log filters, transaction filters, trace filters, block filters, field selection, join modes, streaming |
| Quickstart | pnpx logtui aave arbitrum - zero setup required |
| Powers | HyperIndex, ChainDensity.xyz, Scope.sh, LogTUI, and more |
| Relationship to HyperIndex | HyperSync is the data engine; HyperIndex is the full indexing framework built on top of it |
| Support | Discord · GitHub |
HyperSync: Ultra-Fast & Flexible Data API
File: overview.md
What is HyperSync?
HyperSync is a purpose-built data retrieval layer for onchain data, built from the ground up in Rust. It serves as an alternative to traditional JSON-RPC endpoints, letting you filter and select exactly the data you need. On the workloads in our performance benchmarks, scans that take hours or days over RPC complete in seconds.
HyperSync is Envio's high-performance blockchain data engine that serves as a direct replacement for traditional RPC endpoints, delivering up to 2000x faster data access.
HyperIndex is built on top of HyperSync, providing a complete indexing framework with schema management, event handling, and GraphQL APIs.
Use HyperSync directly when you need raw blockchain data at maximum speed, or use HyperIndex when you need a full-featured indexing solution.
The Problem HyperSync Solves
Traditional blockchain data access through JSON-RPC faces several limitations:
- Speed constraints: Retrieving large amounts of historical data can take days
- Query flexibility: Complex data analysis requires many separate calls
- Cost inefficiency: Expensive for data-intensive applications
Key Benefits
- Exceptional Performance: Retrieve and process blockchain data up to 1000x faster than traditional RPC methods
- Comprehensive Coverage: Access data across EVM chains and Fuel, with new networks added regularly
- Flexible Query Capabilities: Filter, select, and process exactly the data you need with powerful query options
- Cost Efficiency: Dramatically reduce infrastructure costs for data-intensive applications
- Simple Integration: Client libraries available for Python, Rust, Node.js, and Go
Performance Benchmarks
HyperSync delivers transformative performance compared to traditional methods:
| Task | Traditional RPC | HyperSync | Improvement |
|---|---|---|---|
| Scan Arbitrum blockchain for sparse log data | Hours/Days | 2 seconds | ~2000x faster |
| Fetch all Uniswap v3 PoolCreated events ethereum | Hours | Seconds | ~500x faster |
Use Cases
HyperSync powers a wide range of blockchain applications, enabling developers to build tools that would be impractical with traditional data access methods:
General Applications
- Blockchain Indexers: Build high-performance data indexers with minimal infrastructure
- Data Analytics: Perform complex on-chain analysis in seconds instead of days
- Block Explorers: Create responsive explorers with comprehensive data access
- Monitoring Tools: Track blockchain activity with near real-time updates
- Cross-chain Applications: Access unified data across multiple networks
- ETL Pipelines: Create pipelines to extract and save data fast
Powered by HyperSync
HyperIndex
- 100x faster blockchain indexing across EVM chains and Fuel
- Powers 100 plus applications like v4.xyz analytics
ChainDensity.xyz
- Fast transaction/event density analysis for any address
- Generates insights in seconds that would take hours with traditional methods
Scope.sh
- Ultra-fast Account Abstraction (AA) focused block explorer
- Fast historical data retrieval with minimal latency
LogTUI
- Terminal-based UI for finding all historical blockchain events
- Built-in presets for 20+ protocols (Uniswap, Chainlink, Aave, ENS, etc.)
- Try it:
pnpx logtui aave arbitrumto track Aave events on Arbitrum in your terminal
See HyperSync in Action
Next Steps
- Try the Quick Start Guide to get up and running in minutes
- Build queries visually with our Intuitive Query Builder
- Get an API Token to access HyperSync services
- View Supported Networks to see available chains
- Check Client Documentation for language-specific guides
- Join our Discord for support and updates
Our documentation is continuously improving! If you have questions or need assistance, please reach out in our Discord community.
Quickstart
File: quickstart.md
The HyperSync Query Builder lets you construct and run queries directly in your browser — no install, no code required. It's the fastest way to get familiar with HyperSync and see what's possible.
Get up and running with HyperSync in minutes. This guide will help you start accessing blockchain data at unprecedented speeds with minimal setup. For a conceptual overview before you dive in, see What is HyperSync?.
Quickest Start: Try LogTUI
Want to see HyperSync in action with zero setup? Try LogTUI, a terminal-based blockchain event viewer:
# Monitor Aave events on Arbitrum (no installation needed)
pnpx logtui aave arbitrum
Clone the Quickstart Repository
The fastest way to get started is to clone our minimal example repository:
git clone https://github.com/enviodev/hypersync-quickstart.git
cd hypersync-quickstart
This repository contains everything you need to start streaming blockchain data using HyperSync.
Install Dependencies
# Using pnpm (recommended)
pnpm install
Choose Your Adventure
The repository includes three different script options, all of which retrieve Uniswap V3 events from Ethereum mainnet:
# Run minimal version (recommended for beginners)
node run-simple.js
# Run full version with progress bar
node run.js
# Run version with terminal UI
node run-tui.js
That's it! You're now streaming data directly from Ethereum mainnet through HyperSync! (TUI version below)
Understanding the Code
Let's look at the core concepts in the example code:
1. Initialize the Client
// Initialize Hypersync client
const client = new HypersyncClient({
url: "https://eth.hypersync.xyz", // Change this URL for different networks
apiToken: process.env.ENVIO_API_TOKEN!,
});
Note: To connect to different networks, see the Supported Networks page for a complete list of available URLs.
2. Build Your Query
The heart of HyperSync is the query object, which defines what data you want to retrieve:
let query = {
fromBlock: 0, // Start block (0 = genesis)
logs: [
// Filter for specific events
{
topics: [topic0_list], // Event signatures we're interested in
},
],
fieldSelection: {
// Only return fields we need
log: [
"Data",
"Address",
"Topic0",
"Topic1",
"Topic2",
"Topic3",
],
},
};
3. Stream and Process Results
// Start streaming events
const stream = await client.stream(query, {});
while (true) {
const res = await stream.recv();
// Process results
if (res.data && res.data.logs) {
// Do something with the logs
totalEvents += res.data.logs.length;
}
// Update starting block for next batch
if (res.nextBlock) {
query.fromBlock = res.nextBlock;
}
}
Key Concepts for Building Queries
Filtering Data
HyperSync lets you filter blockchain data in several ways:
- Log filters: Find specific events by contract address and event signature
- Transaction filters: Filter by sender/receiver addresses, method signatures, etc.
- Trace filters: Access internal transactions and state changes (only supported on select networks like Ethereum Mainnet)
- Block filters: Get data from specific block ranges
Field Selection
One of HyperSync's most powerful features is the ability to retrieve only the fields you need:
fieldSelection: {
// Block fields
block: ["Number", "Timestamp"],
// Log fields
log: ["Address", "Topic0", "Data"],
// Transaction fields
transaction: ["From", "To", "Value"],
}
This selective approach dramatically reduces unnecessary data transfer and improves performance.
Join Modes
HyperSync allows you to control how related data is joined:
- JoinNothing: Return only exact matches
- JoinAll: Return matches plus all related objects
- JoinTransactions: Return matches plus their transactions
- Default: Return a reasonable set of related objects
Examples
Finding Uniswap V3 Events
This example (from the quickstart repo) streams all Uniswap V3 events from the beginning of Ethereum:
// Define Uniswap V3 event signatures
const event_signatures = [
"PoolCreated(address,address,uint24,int24,address)",
"Burn(address,int24,int24,uint128,uint256,uint256)",
"Initialize(uint160,int24)",
"Mint(address,address,int24,int24,uint128,uint256,uint256)",
"Swap(address,address,int256,int256,uint160,uint128,int24)",
];
// Create topic0 hashes from event signatures
const topic0_list = event_signatures.map((sig) => keccak256(toHex(sig)));
// Initialize Hypersync client
const client = new HypersyncClient({
url: "https://eth.hypersync.xyz",
apiToken: process.env.ENVIO_API_TOKEN!,
});
// Define query for Uniswap V3 events
let query = {
fromBlock: 0,
logs: [
{
topics: [topic0_list],
},
],
fieldSelection: {
log: [
"Data",
"Address",
"Topic0",
"Topic1",
"Topic2",
"Topic3",
],
},
};
const main = async () => {
console.log("Starting Uniswap V3 event scan...");
const stream = await client.stream(query, {});
// Process stream...
};
main();
Supported Networks
HyperSync supports EVM-compatible networks. You can change networks by simply changing the client URL:
// Ethereum Mainnet
const client = new HypersyncClient({
url: "https://eth.hypersync.xyz",
apiToken: process.env.ENVIO_API_TOKEN!,
});
// Arbitrum
const client = new HypersyncClient({
url: "https://arbitrum.hypersync.xyz",
apiToken: process.env.ENVIO_API_TOKEN!,
});
// Base
const client = new HypersyncClient({
url: "https://base.hypersync.xyz",
apiToken: process.env.ENVIO_API_TOKEN!,
});
See the Supported Networks page for a complete list.
Using LogTUI
This quickstart repository powers LogTUI, a terminal-based blockchain event viewer built on HyperSync. LogTUI lets you monitor events from popular protocols across multiple chains with zero configuration.
Try it with a single command:
# Monitor Uniswap events on unichain
pnpx logtui uniswap-v4 unichain
# Monitor Aave events on Arbitrum
pnpx logtui aave arbitrum
# See all available options
pnpx logtui --help
LogTUI supports scanning historically for any events across all networks supported by HyperSync.
Next Steps
You're now ready to build with HyperSync! Here are some resources for diving deeper:
- Client Libraries - Explore language-specific clients
- Query Reference - Learn advanced query techniques
- Build queries visually - Use our Intuitive Query Builder
- curl Examples - Test queries directly in your terminal
- Complete Getting Started Guide - More comprehensive guidance
API Token
An API token is required to use HyperSync. Get an API token and set it as an environment variable:
export ENVIO_API_TOKEN="your-api-token-here"
Congratulations! You've taken your first steps with HyperSync, bringing ultra-fast blockchain data access to your applications. Happy building!
Getting Started with HyperSync
File: hypersync-usage.md
The HyperSync Query Builder lets you construct and run queries directly in your browser — no install, no code required. It's the fastest way to get familiar with HyperSync and see what's possible.
HyperSync is Envio's high-performance blockchain data engine that provides up to 2000x faster access to blockchain data compared to traditional RPC endpoints. This guide will help you understand how to effectively use HyperSync in your applications.
Quick Start Video
Watch this quick tutorial to see HyperSync in action:
Core Concepts
HyperSync revolves around two main concepts:
- Queries - Define what blockchain data you want to retrieve
- Output Configuration - Specify how you want that data formatted and delivered
Think of queries as your data filter and the output configuration as your data processor.
Building Effective Queries
Queries are the heart of working with HyperSync. They allow you to filter for specific blocks, logs, transactions, and traces.
Query Structure
A basic HyperSync query contains:
query = hypersync.Query(
from_block=12345678, # Required: Starting block number
to_block=12345778, # Optional: Ending block number
field_selection=field_selection, # Required: What fields to return
logs=[log_selection], # Optional: Filter for specific logs
transactions=[tx_selection], # Optional: Filter for specific transactions
traces=[trace_selection], # Optional: Filter for specific traces
include_all_blocks=False, # Optional: Include blocks with no matches
max_num_blocks=1000, # Optional: Limit number of blocks processed
max_num_transactions=5000, # Optional: Limit number of transactions processed
max_num_logs=5000, # Optional: Limit number of logs processed
max_num_traces=5000 # Optional: Limit number of traces processed
)
Field Selection
Field selection allows you to specify exactly which data fields you want to retrieve. This improves performance by only fetching what you need:
field_selection = hypersync.FieldSelection(
# Block fields you want to retrieve
block=[
BlockField.NUMBER,
BlockField.TIMESTAMP,
BlockField.HASH
],
# Transaction fields you want to retrieve
transaction=[
TransactionField.HASH,
TransactionField.FROM,
TransactionField.TO,
TransactionField.VALUE
],
# Log fields you want to retrieve
log=[
LogField.ADDRESS,
LogField.TOPIC0,
LogField.TOPIC1,
LogField.TOPIC2,
LogField.TOPIC3,
LogField.DATA,
LogField.TRANSACTION_HASH
],
# Trace fields you want to retrieve (if applicable)
trace=[
TraceField.FROM,
TraceField.TO,
TraceField.VALUE
]
)
Filtering for Specific Data
For most use cases, you'll want to filter for specific logs, transactions, or traces:
Log Selection Example
# Filter for Transfer events from USDC contract
log_selection = hypersync.LogSelection(
address=["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"], # USDC contract
topics=[
["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] # Transfer event signature
]
)
Transaction Selection Example
# Filter for transactions to the Uniswap V3 router
tx_selection = hypersync.TransactionSelection(
to=["0xE592427A0AEce92De3Edee1F18E0157C05861564"] # Uniswap V3 Router
)
Processing the Results
HyperSync provides multiple ways to process query results:
Stream to Parquet Files
Parquet is the recommended format for large data sets:
# Configure output format
parquet_config = hypersync.StreamConfig(
hex_output=hypersync.HexOutput.PREFIXED,
event_signature="Transfer(address indexed from, address indexed to, uint256 value)"
)
# Stream results to a Parquet file
await client.collect_parquet("data_directory", query, parquet_config)
event_signature is accepted by the parquet and arrow methods, collect_parquet
(which writes an extra decoded_logs.parquet alongside the raw output),
collect_arrow and stream_arrow. The methods returning plain Python objects
reject it: collect, collect_events, stream and stream_events all raise
config.event_signature can't be passed to simple type function. With those,
decode explicitly instead, as shown below.
Process Data in Memory
For immediate processing, stream the results and decode the logs yourself:
# Build a decoder for the events you care about
decoder = hypersync.Decoder([
"Transfer(address indexed from, address indexed to, uint256 value)"
])
stream = await client.stream(query, hypersync.StreamConfig())
while True:
res = await stream.recv()
if res is None: # reached the end of the requested range
break
for log in await decoder.decode_logs(res.data.logs):
if log is None: # a log that doesn't match the signature
continue
# Indexed parameters land in `indexed`, the rest in `body`
print(f"Transfer from {log.indexed[0].val} to {log.indexed[1].val} of {log.body[0].val}")
Tips and Best Practices
Performance Optimization
-
Use Appropriate Batch Sizes: Adjust batch size based on your chain and use case:
config = hypersync.StreamConfig(
hex_output=hypersync.HexOutput.PREFIXED,
batch_size=1000000, # Process 1M blocks at a time
concurrency=10, # Use 10 concurrent workers
) -
Enable Trace Logs: Set
RUST_LOG=traceto see detailed progress:export RUST_LOG=trace -
Paginate Large Queries: HyperSync requests have a 5-second time limit. For large data sets, paginate results:
current_block = start_block
while current_block < end_block:
query.from_block = current_block
query.to_block = min(current_block + 1000000, end_block)
result = await client.collect_parquet("data", query, config)
current_block = result.end_block + 1
Network-Specific Considerations
- High-Volume Networks: For networks like Ethereum Mainnet, use smaller block ranges or more specific filters
- Low-Volume Networks: For smaller chains, you can process the entire chain in one query
Complete Example
Here's a complete example that fetches all USDC Transfer events:
import hypersync
from hypersync import (
LogSelection,
LogField,
BlockField,
FieldSelection,
TransactionField,
HexOutput
)
import asyncio
async def collect_usdc_transfers():
# Initialize client
client = hypersync.HypersyncClient(
hypersync.ClientConfig(
url="https://eth.hypersync.xyz",
bearer_token="your-token-here", # Get from https://docs.envio.dev/docs/HyperSync/api-tokens
)
)
# Define field selection
field_selection = hypersync.FieldSelection(
block=[BlockField.NUMBER, BlockField.TIMESTAMP],
transaction=[TransactionField.HASH],
log=[
LogField.ADDRESS,
LogField.TOPIC0,
LogField.TOPIC1,
LogField.TOPIC2,
LogField.DATA,
]
)
# Define query for USDC transfers
query = hypersync.Query(
from_block=12000000,
to_block=12100000,
field_selection=field_selection,
logs=[
LogSelection(
address=["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"], # USDC contract
topics=[
["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] # Transfer signature
]
)
]
)
# Configure output
config = hypersync.StreamConfig(
hex_output=HexOutput.PREFIXED,
event_signature="Transfer(address indexed from, address indexed to, uint256 value)"
)
# Collect data to a Parquet file
result = await client.collect_parquet("usdc_transfers", query, config)
print(f"Processed blocks {query.from_block} to {result.end_block}")
asyncio.run(collect_usdc_transfers())
Decoding Event Logs
When working with blockchain data, event logs contain encoded data that needs to be properly decoded to extract meaningful information. HyperSync provides powerful decoding capabilities to simplify this process.
Understanding Log Structure
Event logs in Ethereum have the following structure:
- Address: The contract that emitted the event
- Topic0: The event signature hash (keccak256 of the event signature)
- Topics 1-3: Indexed parameters (up to 3)
- Data: Non-indexed parameters packed together
Using the Decoder
HyperSync's client libraries include a Decoder class that can parse these raw logs into structured data:
// Create a decoder with event signatures
const decoder = Decoder.fromSignatures([
"Transfer(address indexed from, address indexed to, uint256 amount)",
"Approval(address indexed owner, address indexed spender, uint256 amount)",
]);
// Decode logs
const decodedLogs = await decoder.decodeLogs(logs);
Single vs. Multiple Event Types
HyperSync provides flexibility to decode different types of event logs:
-
Single Event Type: For processing one type of event (e.g., only Swap events)
- See complete example: run-decoder.js
-
Multiple Event Types: For processing different events from the same contract (e.g., Transfer and Approval)
- See complete example: run-decoder-multi.js
Working with Decoded Data
After decoding, you can access the log parameters in a structured way:
- Indexed parameters: Available in
decodedLog.indexedarray - Non-indexed parameters: Available in
decodedLog.bodyarray
Each parameter object contains:
- name: The parameter name from the signature
- type: The Solidity type
- val: The actual value
For example, to access parameters from a Transfer event:
// Access indexed parameters (from, to)
const from = decodedLog.indexed[0]?.val.toString();
const to = decodedLog.indexed[1]?.val.toString();
// Access non-indexed parameters (amount)
const amount = decodedLog.body[0]?.val.toString();
Benefits of Using the Decoder
- Type Safety: Values are properly converted to their corresponding types
- Simplified Access: Direct access to named parameters
- Batch Processing: Decode multiple logs with a single call
- Multiple Event Support: Handle different event types in the same processing pipeline
Next Steps
Now that you understand the basics of using HyperSync:
- Browse the Python Client or other language-specific clients
- Learn about advanced query options
- Tune throughput, and read your rate-limit quota from code, in Stream Config & Tuning
- See example queries for common use cases
- Get your API token to start building
For detailed API references and examples in other languages, check our client documentation.
HyperSync Client Libraries
File: hypersync-clients.md
HyperSync provides powerful client libraries that enable you to integrate high-performance blockchain data access into your applications. These libraries handle the communication with HyperSync servers, data serialization/deserialization, and provide convenient APIs for querying blockchain data.
Quick Links
| Client | Resources |
|---|---|
| Node.js | 📝 API Docs · 📦 NPM · 💻 GitHub · 🧪 Examples |
| Python | 📦 PyPI · 💻 GitHub · 🧪 Examples |
| Rust | 📦 Crates.io · 📝 API Docs · 💻 GitHub · 🧪 Examples |
| Go (community) | 💻 GitHub · 🧪 Examples |
| Solana (Rust) | 📖 Guide · 📦 Crates.io · 📝 API Docs · 💻 GitHub |
| API Tokens | 🔑 Get Tokens |
Client Overview
All HyperSync clients share these key features:
- High Performance: Built on a common Rust foundation for maximum efficiency
- Optimized Transport: Uses binary formats to minimize bandwidth and maximize throughput
- Consistent Experience: Similar APIs across all language implementations
- Automatic Pagination: Handles large data sets efficiently
- Event Decoding: Parses binary event data into structured formats
Choose the client that best matches your application's technology stack:
| Feature | Node.js | Python | Rust | Go |
|---|---|---|---|---|
| Async Support | ✅ | ✅ | ✅ | ✅ |
| Typing | TypeScript | Type Hints | Native | Native |
| Data Formats | JSON, Parquet | JSON, Parquet, CSV | JSON, Parquet | JSON, Parquet |
| Memory Efficiency | Good | Better | Best | Better |
| Installation | npm | pip | cargo | go get |
Node.js Client
The Node.js client provides a TypeScript-first experience for JavaScript developers.
Installation
# Using npm
npm install @envio-dev/hypersync-client
# Using yarn
yarn add @envio-dev/hypersync-client
# Using pnpm
pnpm add @envio-dev/hypersync-client
Python Client
The Python client provides a Pythonic interface with full type hinting support.
Installation
pip install hypersync
Rust Client
The Rust client provides the most efficient and direct access to HyperSync, with all the safety and performance benefits of Rust.
Installation
Add the following to your Cargo.toml:
[dependencies]
hypersync-client = "0.1"
tokio = { version = "1", features = ["full"] }
Solana Client
- 📖 Solana Client Guide
- 📦 Crates.io Package
- 📝 API Documentation
- 💻 GitHub Repository
Solana HyperSync has its own client, because the query and table model is Solana-shaped (slots, instruction calls, account activity) rather than EVM-shaped. It is Rust today, with unpublished Node bindings in the same repository. Everything client-side is covered in the Solana client guide.
[dependencies]
hypersync-client-solana = "0.2"
Go Client
The Go client is community maintained and marked as work-in-progress. For production use, you may want to test thoroughly or consider the officially supported clients.
The Go client provides a native Go interface for accessing HyperSync, with support for streaming and decoding blockchain data.
Installation
go get github.com/enviodev/hypersync-client-go
Using API Tokens
You'll need an API token to use any HyperSync client. Get your token here.
All HyperSync clients require an API token for authentication. Tokens are used to manage access and usage limits.
To get an API token:
- Visit Envio
- Register or sign in with your github account
- Navigate to the API Tokens section
- Create a new token
For detailed instructions, see our API Tokens guide.
Client Selection Guide
Choose the client that best fits your use case:
Choose when: You're building JavaScript/TypeScript applications or if your team is most comfortable with the JavaScript ecosystem.
Choose when: You're doing data science work, need integration with pandas/numpy, or if your team prefers Python's simplicity.
Choose when: You need maximum performance, are doing systems programming, or building performance-critical applications.
Choose when: You're working in a Go ecosystem and want native integration with Go applications. Note that this client is community maintained.
Additional Resources
- 📚 HyperSync Usage Guide
- 📝 Query Reference
- 🧪 cURL Examples
- 📊 Supported Networks
- 🧱 HyperIndex blockchain indexer, the full framework with schema, handlers, and a hosted GraphQL API, powered by HyperSync
Support
Need help getting started or have questions about our clients? Connect with our community:
HyperSync Query
File: hypersync-query.md
This guide explains how to structure queries for HyperSync to efficiently retrieve blockchain data. You'll learn both the basics and advanced techniques to make the most of HyperSync's powerful querying capabilities.
Not all features implemented in HyperSync are available in HyperFuel (the Fuel implementation of HyperSync). For example, as of this writing, stream and collect functions aren't implemented in the Fuel client.
Client Examples
HyperSync offers client libraries in multiple languages, each with its own comprehensive examples. Instead of providing generic examples here, we recommend exploring the language-specific examples:
| Client | Example Links |
|---|---|
| Node.js | Example Repository |
| Python | Example Repository |
| Rust | Example Repository |
| Go | Example Repository |
Additionally, we maintain a comprehensive collection of real-world examples covering various use cases across different languages:
- 30 HyperSync Examples - A diverse collection of practical examples demonstrating HyperSync's capabilities in Python, JavaScript, TypeScript, Rust, and more.
For more details on client libraries, see the HyperSync Clients documentation.
Need help building queries? Try our Intuitive Query Builder to construct queries visually and see the results in real-time.
Set the RUST_LOG environment variable to trace for more detailed logs when using client libraries.
Table of Contents
- Understanding HyperSync Queries
- Query Execution Process
- Query Structure Reference
- Data Schema
- Response Structure
- Stream and Collect Functions
- Working with Join Modes
- Best Practices
Understanding HyperSync Queries
A HyperSync query defines what blockchain data you want to retrieve and how you want it returned. Unlike regular RPC calls, HyperSync queries offer:
- Flexible filtering across logs, transactions, traces, and blocks
- Field selection to retrieve only the data you need
- Automatic pagination to handle large result sets
- Join capabilities that link related blockchain data together
Core Concepts
- Selections: Define criteria for filtering blockchain data (logs, transactions, traces)
- Field Selection: Specify which fields to include in the response
- Limits: Control query execution time and response size
- Joins: Determine how related data is connected in the response
Query Execution Process
How Data is Organized
HyperSync organizes blockchain data into groups of contiguous blocks. When executing a query:
- The server identifies which block group contains the starting block
- It processes data groups sequentially until it hits a limit
- Results are returned along with a
next_blockvalue for pagination
Query Limits
HyperSync enforces several types of limits to ensure efficient query execution:
| Limit Type | Description | Behavior |
|---|---|---|
| Time | Server-configured maximum execution time | May slightly exceed limit to complete current block group |
| Response Size | Maximum data returned | May slightly exceed limit to complete current block group |
| to_block | User-specified ending block (exclusive) | Never exceeds this limit |
| maxnum* | User-specified maximum number of results by type | May slightly exceed limit to complete current block group |
Execution Steps
- Server receives query and identifies the starting block group
- It scans each block group, applying selection criteria
- It joins related data according to the specified join mode
- When a limit is reached, it finishes processing the current block group
- It returns results with pagination information
Understanding Pagination
HyperSync uses a time-based pagination model that differs from traditional RPC calls:
- By default, HyperSync has a 5-second query execution limit
- Within this time window, it processes as many blocks as possible
- For example, starting with
from_block: 0might progress to block 10 million in a single request - Each response includes a
next_blockvalue indicating where to resume for the next query - This differs from RPC calls where you typically specify fixed block ranges (e.g., 0-1000)
Understanding nextBlock
nextBlock is the block number immediately after the last block included in the response. Use it as the fromBlock of your next query if you want to continue scanning. Resuming from nextBlock gives you a continuous, non-overlapping scan—no gaps, no duplicates.
Usage pattern: Call get or getEvents, process the page, then if nextBlock is less than your desired end (toBlock or archiveHeight), set fromBlock = nextBlock and repeat:
let query = { fromBlock: 0, logs: [...], fieldSelection: {...} };
while (true) {
const res = await client.get(query);
// Process res.data...
const targetEnd = query.toBlock ?? res.archiveHeight;
if (res.nextBlock >= targetEnd) break;
query = { ...query, fromBlock: res.nextBlock };
}
For most use cases, the stream function handles pagination automatically, making it the recommended approach for processing large ranges of blocks.
Reverse Search
HyperSync supports searching from the head of the chain backwards, which is useful for:
- Block explorers showing the most recent activity
- UIs displaying latest transactions for a user
- Any use case where recent data is more relevant
To use reverse search, add the reverse: true parameter to your stream call:
// Example of reverse search to get recent transactions
const receiver = await client.stream(query, { reverse: true });
let count = 0;
while (true) {
let res = await receiver.recv();
if (res === null) {
break;
}
for (const tx of res.data.transactions) {
console.log(JSON.stringify(tx, null, 2));
}
count += res.data.transactions.length;
if (count >= 20) {
break;
}
}
With reverse search, HyperSync starts from the latest block and works backwards, allowing you to efficiently access the most recent blockchain data first.
Query Structure Reference
A complete HyperSync query can include the following components:
Core Query Parameters
struct Query {
/// The block to start the query from
from_block: u64,
/// The block to end the query at (exclusive)
/// If not specified, the query runs until the end of available data
to_block: Optional<u64>,
/// Log selection criteria (OR relationship between selections)
logs: Array<LogSelection>,
/// Transaction selection criteria (OR relationship between selections)
transactions: Array<TransactionSelection>,
/// Trace selection criteria (OR relationship between selections)
traces: Array<TraceSelection>,
/// Whether to include all blocks in the requested range
/// Default: only return blocks related to matched transactions/logs
include_all_blocks: bool,
/// Fields to include in the response
field_selection: FieldSelection,
/// Maximum results limits (approximate)
max_num_blocks: Optional<usize>,
max_num_transactions: Optional<usize>,
max_num_logs: Optional<usize>,
max_num_traces: Optional<usize>,
/// Data relationship model (Default, JoinAll, or JoinNothing)
join_mode: JoinMode,
}
Selection Types
Log Selection
struct LogSelection {
/// Contract addresses to match (empty = match all)
address: Array<Address>,
/// Topics to match by position (empty = match all)
/// Each array element corresponds to a topic position (0-3)
/// Within each position, any matching value will satisfy the condition
topics: Array<Array<Topic>>,
}
Transaction Selection
struct TransactionSelection {
/// Sender addresses (empty = match all)
/// Has AND relationship with 'to' field
from: Array<Address>,
/// Recipient addresses (empty = match all)
/// Has AND relationship with 'from' field
to: Array<Address>,
/// Method signatures to match (first 4 bytes of input)
sighash: Array<Sighash>,
/// Transaction status to match (1 = success, 0 = failure)
status: Optional<u8>,
/// Transaction types to match (e.g., 0 = legacy, 2 = EIP-1559)
type: Array<u8>,
/// Created contract addresses to match
contract_address: Array<Address>,
}
Block Selection
struct BlockSelection {
/// Block hashes to match (empty = match all)
hash: Array<Hash>,
/// Miner/validator addresses to match (empty = match all)
miner: Array<Address>,
}
Trace Selection
struct TraceSelection {
/// Sender addresses (empty = match all)
/// Has AND relationship with 'to' field
from: Array<Address>,
/// Recipient addresses (empty = match all)
/// Has AND relationship with 'from' field
to: Array<Address>,
/// Created contract addresses to match
address: Array<Address>,
/// Call types to match (e.g., "call", "delegatecall")
call_type: Array<String>,
/// Reward types to match (e.g., "block", "uncle")
reward_type: Array<String>,
/// Trace types to match (e.g., "call", "create", "suicide", "reward")
kind: Array<String>,
/// Method signatures to match (first 4 bytes of input)
sighash: Array<Sighash>,
}
Field Selection
struct FieldSelection {
/// Block fields to include in response
block: Array<String>,
/// Transaction fields to include in response
transaction: Array<String>,
/// Log fields to include in response
log: Array<String>,
/// Trace fields to include in response
trace: Array<String>,
}
Data Schema
HyperSync organizes blockchain data into four main tables. Below are the available fields for each table.
When specifying fields in your query, always use snake_case names (e.g., block_number, not blockNumber).
Block Fields
class BlockField(StrEnum):
# Fields present on all EVM chains
NUMBER = 'number' # Block number
HASH = 'hash' # Block hash
PARENT_HASH = 'parent_hash' # Parent block hash
SHA3_UNCLES = 'sha3_uncles' # SHA3 of uncles data
LOGS_BLOOM = 'logs_bloom' # Bloom filter for logs
TRANSACTIONS_ROOT = 'transactions_root' # Root of transaction trie
STATE_ROOT = 'state_root' # Root of state trie
RECEIPTS_ROOT = 'receipts_root' # Root of receipts trie
MINER = 'miner' # Miner/validator address
EXTRA_DATA = 'extra_data' # Extra data field
SIZE = 'size' # Block size in bytes
GAS_LIMIT = 'gas_limit' # Block gas limit
GAS_USED = 'gas_used' # Total gas used in block
TIMESTAMP = 'timestamp' # Block timestamp (Unix time)
# Optional fields — not present on all EVM chains (may be null)
NONCE = 'nonce' # Block nonce (absent on some L2s)
DIFFICULTY = 'difficulty' # Block difficulty (PoW chains only)
TOTAL_DIFFICULTY = 'total_difficulty' # Total chain difficulty (PoW chains only)
UNCLES = 'uncles' # Uncle block hashes (absent on some L2s)
MIX_HASH = 'mix_hash' # Mix hash (absent on some L2s)
BASE_FEE_PER_GAS = 'base_fee_per_gas' # EIP-1559 base fee (post-London chains only)
BLOB_GAS_USED = 'blob_gas_used' # Total blob gas used (EIP-4844 chains only)
EXCESS_BLOB_GAS = 'excess_blob_gas' # Excess blob gas (EIP-4844 chains only)
PARENT_BEACON_BLOCK_ROOT = 'parent_beacon_block_root' # Parent beacon block root (EIP-4844 chains only)
WITHDRAWALS_ROOT = 'withdrawals_root' # Root of withdrawals trie (post-Shanghai chains only)
WITHDRAWALS = 'withdrawals' # Validator withdrawals (post-Shanghai chains only)
L1_BLOCK_NUMBER = 'l1_block_number' # L1 block number (Arbitrum only)
SEND_COUNT = 'send_count' # Send count (Arbitrum only)
SEND_ROOT = 'send_root' # Send root (Arbitrum only)
Transaction Fields
class TransactionField(StrEnum):
# Block-related fields
BLOCK_HASH = 'block_hash' # The Keccak 256-bit hash of the block
BLOCK_NUMBER = 'block_number' # Block number containing the transaction
# Transaction identifiers
HASH = 'hash' # Transaction hash (keccak hash of RLP encoded signed transaction)
TRANSACTION_INDEX = 'transaction_index' # Index of the transaction in the block
# Transaction participants
FROM = 'from' # 160-bit address of the sender
TO = 'to' # 160-bit address of the recipient (null for contract creation)
# Gas information
GAS = 'gas' # Gas limit set by sender
GAS_PRICE = 'gas_price' # Wei paid per unit of gas
GAS_USED = 'gas_used' # Actual gas used by the transaction
CUMULATIVE_GAS_USED = 'cumulative_gas_used' # Total gas used in the block up to this transaction
EFFECTIVE_GAS_PRICE = 'effective_gas_price' # Sum of base fee and tip paid per unit of gas
# EIP-1559 fields
MAX_PRIORITY_FEE_PER_GAS = 'max_priority_fee_per_gas' # Max priority fee (a.k.a. GasTipCap)
MAX_FEE_PER_GAS = 'max_fee_per_gas' # Max fee per gas (a.k.a. GasFeeCap)
# Transaction data
INPUT = 'input' # Transaction input data or contract initialization code
VALUE = 'value' # Amount of ETH transferred in wei
NONCE = 'nonce' # Number of transactions sent by the sender
# Signature fields
V = 'v' # Replay protection value (based on chain_id)
R = 'r' # The R field of the signature
S = 's' # The S field of the signature
Y_PARITY = 'y_parity' # Signature Y parity
CHAIN_ID = 'chain_id' # Chain ID for replay protection (EIP-155)
# Contract-related fields
CONTRACT_ADDRESS = 'contract_address' # Address of created contract (for contract creation txs)
# Transaction result fields
STATUS = 'status' # Success (1) or failure (0)
LOGS_BLOOM = 'logs_bloom' # Bloom filter for logs produced by this transaction
ROOT = 'root' # State root (pre-Byzantium)
# EIP-2930 fields
ACCESS_LIST = 'access_list' # List of addresses and storage keys to pre-warm
# EIP-4844 (blob transactions) fields
MAX_FEE_PER_BLOB_GAS = 'max_fee_per_blob_gas' # Max fee per data gas (blob fee cap)
BLOB_VERSIONED_HASHES = 'blob_versioned_hashes' # List of blob versioned hashes
# Transaction type
KIND = 'type' # Transaction type (0=legacy, 1=EIP-2930, 2=EIP-1559, 3=EIP-4844, 4=EIP-7702) # note - in old versions of the clients this was called 'kind', in newer versions its called 'type'
# L2-specific fields (for rollups)
L1_FEE = 'l1_fee' # Fee for L1 data (L1GasPrice × L1GasUsed)
L1_GAS_PRICE = 'l1_gas_price' # Gas price on L1
L1_GAS_USED = 'l1_gas_used' # Amount of gas consumed on L1
L1_FEE_SCALAR = 'l1_fee_scalar' # Multiplier for L1 fee calculation
GAS_USED_FOR_L1 = 'gas_used_for_l1' # Gas spent on L1 calldata in L2 gas units
Log Fields
class LogField(StrEnum):
# Log identification
LOG_INDEX = 'log_index' # Index of the log in the block
TRANSACTION_INDEX = 'transaction_index' # Index of the transaction in the block
# Transaction information
TRANSACTION_HASH = 'transaction_hash' # Hash of the transaction that created this log
# Block information
BLOCK_HASH = 'block_hash' # Hash of the block containing this log
BLOCK_NUMBER = 'block_number' # Block number containing this log
# Log content
ADDRESS = 'address' # Contract address that emitted the event
DATA = 'data' # Non-indexed data from the event
# Topics (indexed parameters)
TOPIC0 = 'topic0' # Event signature hash
TOPIC1 = 'topic1' # First indexed parameter
TOPIC2 = 'topic2' # Second indexed parameter
TOPIC3 = 'topic3' # Third indexed parameter
# Reorg information
REMOVED = 'removed' # True if log was removed due to chain reorganization
Trace Fields
class TraceField(StrEnum):
# Trace identification
TRANSACTION_HASH = 'transaction_hash' # Hash of the transaction
TRANSACTION_POSITION = 'transaction_position' # Index of the transaction in the block
SUBTRACES = 'subtraces' # Number of sub-traces created during execution
TRACE_ADDRESS = 'trace_address' # Array indicating position in the trace tree
# Block information
BLOCK_HASH = 'block_hash' # Hash of the block containing this trace
BLOCK_NUMBER = 'block_number' # Block number containing this trace
# Transaction participants
FROM = 'from' # Address of the sender
TO = 'to' # Address of the recipient (null for contract creation)
# Value and gas
VALUE = 'value' # ETH value transferred (in wei)
GAS = 'gas' # Gas limit
GAS_USED = 'gas_used' # Gas actually used
# Call data
INPUT = 'input' # Call data for function calls
INIT = 'init' # Initialization code for contract creation
OUTPUT = 'output' # Return data from the call
# Contract information
ADDRESS = 'address' # Contract address (for creation/destruction)
CODE = 'code' # Contract code
# Trace types and categorization
TYPE = 'type' # Trace type (call, create, suicide, reward)
CALL_TYPE = 'call_type' # Call type (call, delegatecall, staticcall, etc.)
REWARD_TYPE = 'reward_type' # Reward type (block, uncle)
# Other actors
AUTHOR = 'author' # Address of receiver for reward transactions
# Result information
ERROR = 'error' # Error message if failed
For a complete list of all available fields, refer to the HyperSync API Reference.
Response Structure
When you execute a HyperSync query, the response includes both metadata and the requested data:
struct QueryResponse {
/// Current height of the blockchain in HyperSync
archive_height: Optional<u64>,
/// Block number immediately after the last block included in this response.
/// Use as from_block in your next query for pagination.
next_block: u64,
/// Query execution time in milliseconds
total_execution_time: u64,
/// The actual blockchain data matching your query
data: ResponseData,
/// Information to help handle chain reorganizations
rollback_guard: Optional<RollbackGuard>,
}
The next_block value tells you where to resume scanning. See Understanding nextBlock for a clear definition and usage pattern.
Rollback Guard
The optional rollback_guard lets you detect chain reorganizations (reorgs) between successive queries, so you can re-fetch any data that has become stale.
struct RollbackGuard {
/// Last block scanned in this query
block_number: u64,
/// Timestamp of the last block scanned
timestamp: i64,
/// Hash of the last block scanned
hash: Hash,
/// First block scanned in this query
first_block_number: u64,
/// Parent hash of the first block scanned
first_parent_hash: Hash,
}
The guard is Optional<RollbackGuard>: it is present whenever the response covers blocks near the chain tip (where reorgs can still happen) and absent for queries that return no data.
How HyperSync handles reorgs internally
As HyperSync ingests new blocks it checks each block's parent_hash against the previous block's hash. When a mismatch is detected, HyperSync re-syncs the affected blocks and continues serving the canonical chain.
A single query response is always internally consistent: you will never receive a mix of blocks from different forks. The rollback guard exists to detect reorgs that happen between successive queries, where data you fetched earlier may now be stale.
Detecting a reorg
After each query, store the guard's block_number and hash. On the next query, compare:
previous response.hash(last block you saw)next response.first_parent_hash(parent of the first block in the new batch)
If they match, the chain is intact. If they differ, a reorg occurred somewhere between the two queries.
Query N: rollback_guard.hash = 0xABC... (stored)
Query N+1: rollback_guard.first_parent_hash = 0xABC... match -> no reorg
= 0xDEF... mismatch -> reorg
Recovering from a reorg
The guard tells you that a reorg happened but not how deep. To find the depth, keep enough history to cover your chain's reorg threshold (for example, 200 blocks for Polygon) and walk backwards: re-fetch each stored block's hash and compare. The first block whose hash still matches is the last canonical block; rewind your downstream state to there and resume querying.
history = [] # list of (block_number, hash)
while True:
res = client.get(query)
guard = res.rollback_guard
if guard is None:
process(res.data)
query.from_block = res.next_block
continue
if history and guard.first_parent_hash != history[-1][1]:
# Walk back to find the last block still on chain.
while history:
block_num, stored_hash = history[-1]
if client.get_block_hash(block_num) == stored_hash:
break
history.pop()
rewind_to = history[-1][0] + 1 if history else query.from_block
rollback_state_to(rewind_to)
query.from_block = rewind_to
continue
process(res.data)
history.append((guard.block_number, guard.hash))
cutoff = guard.block_number - REORG_THRESHOLD
history = [(b, h) for b, h in history if b >= cutoff]
query.from_block = res.next_block
HyperIndex handles all of this for you: it tracks recent block hashes, locates the reorg point, and rolls back database state automatically. See Reorgs Support for details.
Stream and Collect Functions
For continuous data processing or building data pipelines, client libraries provide stream and collect functions that wrap the base query functionality.
These functions are not designed for use at the blockchain tip where rollbacks may occur. For real-time data near the chain tip, implement a custom loop using the get functions and handle rollbacks manually.
Stream Function
The stream function:
- Runs multiple queries concurrently
- Returns a stream handle that yields results as they're available
- Optimizes performance through pipelined decoding/decompression
- Continues until reaching either
to_blockor the chain height at stream start
Collect Functions
The collect functions:
- Call
streaminternally and aggregate results - Offer different output formats (JSON, Parquet)
- Handle data that may not fit in memory
Always call close() on stream handles when finished to prevent resource leaks, especially if creating multiple streams.
Working with Join Modes
HyperSync "joins" connect related blockchain data automatically. Unlike SQL joins that combine rows from different tables, HyperSync joins determine which related records to include in the response.
Default Join Mode (logs → transactions → traces → blocks)
With the default join mode:
- When you query logs, you automatically get their associated transactions
- Those transactions' traces are also included
- The blocks containing these transactions are included
┌───────┐ ┌───────────────┐ ┌───────┐ ┌───────┐
│ Logs │ ──> │ Transactions │ ──> │ Traces│ ──> │ Blocks│
└───────┘ └───────────────┘ └───────┘ └───────┘
JoinAll Mode
JoinAll creates a more comprehensive network of related data:
┌─────────────────────────────┐
│ │
▼ │
┌───────┐ <──> ┌───────────────┐ <──> ┌───────┐ <──> ┌───────┐
│ Logs │ │ Transactions │ │ Traces│ │ Blocks│
└───────┘ └───────────────┘ └───────┘ └───────┘
For example, if you query a trace:
- You get the transaction that created it
- You get ALL logs from that transaction (not just the ones matching your criteria)
- You get ALL traces from that transaction
- You get the block containing the transaction
JoinNothing Mode
JoinNothing is the most restrictive:
┌───────┐ ┌───────────────┐ ┌───────┐ ┌───────┐
│ Logs │ │ Transactions │ │ Traces│ │ Blocks│
└───────┘ └───────────────┘ └───────┘ └───────┘
Only data directly matching your selection criteria is returned, with no related records included.
Best Practices
To get the most out of HyperSync queries:
- Minimize field selection - Only request fields you actually need to improve performance
- Use appropriate limits - Set
max_num_*parameters to control response size - Choose the right join mode - Use
JoinNothingfor minimal data,JoinAllfor complete context - Process in chunks - For large datasets, use pagination or the
streamfunction - Consider Parquet - For analytical workloads, use
collect_parquetfor efficient storage - Handle chain tip carefully - Near the chain tip, implement custom rollback handling
HyperSync Preset Queries
File: hypersync-presets.md
HyperSync's client libraries include helper functions that build common queries. These presets are useful when you need raw blockchain objects without crafting a query manually.
Each preset returns a Query object so you can pass it directly to client.get, client.stream, or client.collect.
The names below are the Python ones. Node.js and TypeScript expose the same four presets in camelCase, and Rust uses a different naming scheme again. See Other client libraries.
Available Presets
preset_query_blocks_and_transactions(from_block, to_block=None)
Returns every block and all associated transactions within the supplied block range. Leave to_block unset to query up to the current chain head.
import hypersync
import asyncio
async def main():
client = hypersync.HypersyncClient(
hypersync.ClientConfig(
url="https://eth.hypersync.xyz",
bearer_token="your-token-here", # Get from https://docs.envio.dev/docs/HyperSync/api-tokens
)
)
query = hypersync.preset_query_blocks_and_transactions(17_000_000, 17_000_050)
result = await client.get(query)
print(f"Query returned {len(result.data.blocks)} blocks and {len(result.data.transactions)} transactions")
asyncio.run(main())
preset_query_blocks_and_transaction_hashes(from_block, to_block=None)
Returns each block in the range along with only the transaction hashes.
preset_query_logs(address, from_block, to_block=None)
Fetches all logs emitted by a single contract address in the given block range. The address is the first argument.
logs_res = await client.get(
hypersync.preset_query_logs("0xdAC17F958D2ee523a2206206994597C13D831ec7", 17_000_000, 17_000_050)
)
preset_query_logs_of_event(address, topic0, from_block, to_block=None)
Fetches logs emitted by a contract address that match a single event topic. topic0 is the hashed event signature, not the human-readable one.
# keccak256("Transfer(address,address,uint256)")
transfer_topic0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
logs_res = await client.get(
hypersync.preset_query_logs_of_event(
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
transfer_topic0,
17_000_000,
17_000_050,
)
)
Other client libraries
Node.js and TypeScript
@envio-dev/hypersync-client exports the same four presets in camelCase, with the same argument order. The Python snake_case names are not defined in the Node client.
import {
presetQueryBlocksAndTransactions,
presetQueryBlocksAndTransactionHashes,
presetQueryLogs,
presetQueryLogsOfEvent,
} from "@envio-dev/hypersync-client";
const query = presetQueryLogs(
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
17_000_000,
17_000_050
);
Rust
The Rust client groups these in a preset_query module, drops the preset_query_ prefix, and takes the block range first rather than last.
use hypersync_client::preset_query;
let query = preset_query::logs(17_000_000, Some(17_000_050), contract_address);
The module provides blocks_and_transactions, blocks_and_transaction_hashes, logs, logs_of_event, transactions, and transactions_from_address. The last two have no Python or Node.js equivalent.
For runnable examples, see the Python and Node.js client repositories.
Use these helpers whenever you need a quick query without specifying field selections or joins manually.
Using curl with HyperSync
File: hypersync-curl-examples.md
This guide demonstrates how to interact with HyperSync using direct HTTP requests via curl. These examples provide a quick way to explore HyperSync functionality without installing client libraries.
We highly recommend trying these curl examples as they're super quick and easy to run directly in your terminal. It's one of the fastest ways to experience HyperSync's performance firsthand and see just how quickly you can retrieve blockchain data without any setup overhead. Simply copy, paste, and be amazed by the response speed!
While curl requests are technically slower than our client libraries (since they use HTTP rather than binary data transfer protocols), they're still impressively fast and provide an excellent demonstration of HyperSync's capabilities without any installation requirements.
Table of Contents
Curl vs. Client Libraries
When deciding whether to use curl commands or client libraries, consider the following comparison:
When to Use curl (JSON API)
- Quick Prototyping: Test endpoints and explore data structure without setup
- Simple Scripts: Perfect for shell scripts and automation
- Language Independence: When working with languages without HyperSync client libraries
- API Exploration: When learning the HyperSync API capabilities
When to Use Client Libraries
- Production Applications: For stable, maintained codebases
- Complex Data Processing: When working with large datasets or complex workflows
- Performance: Client libraries offer automatic compression and pagination
- Error Handling: Built-in retry mechanisms and better error reporting
- Data Formats: Support for efficient formats like Apache Arrow
Common Use Cases
Get All ERC-20 Transfers for an Address
This example filters for all ERC-20 transfer events involving a specific address, either as sender or recipient. Feel free to swap your address into the example.
What this query does:
- Filters logs for the Transfer event signature (topic0)
- Matches when the address appears in either topic1 (sender) or topic2 (recipient)
- Also includes direct transactions to/from the address
curl --request POST \
--url https://eth.hypersync.xyz/query \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"from_block": 0,
"logs": [
{
"topics": [
[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
],
[],
[
"0x0000000000000000000000001e037f97d730Cc881e77F01E409D828b0bb14de0"
]
]
},
{
"topics": [
[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
],
[
"0x0000000000000000000000001e037f97d730Cc881e77F01E409D828b0bb14de0"
],
[]
]
}
],
"transactions": [
{
"from": [
"0x1e037f97d730Cc881e77F01E409D828b0bb14de0"
]
},
{
"to": [
"0x1e037f97d730Cc881e77F01E409D828b0bb14de0"
]
}
],
"field_selection": {
"block": [
"number",
"timestamp",
"hash"
],
"log": [
"block_number",
"log_index",
"transaction_index",
"data",
"address",
"topic0",
"topic1",
"topic2",
"topic3"
],
"transaction": [
"block_number",
"transaction_index",
"hash",
"from",
"to",
"value",
"input"
]
}
}'
Get All Logs for a Smart Contract
This example retrieves all event logs emitted by a specific contract (USDC in this case).
Key points:
- Sets
from_block: 0to scan from the beginning of the chain - Uses
next_blockin the response for pagination to fetch subsequent data - Includes relevant block, log, and transaction fields
curl --request POST \
--url https://eth.hypersync.xyz/query \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"from_block": 0,
"logs": [
{
"address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"]
}
],
"field_selection": {
"block": [
"number",
"timestamp",
"hash"
],
"log": [
"block_number",
"log_index",
"transaction_index",
"data",
"address",
"topic0",
"topic1",
"topic2",
"topic3"
],
"transaction": [
"block_number",
"transaction_index",
"hash",
"from",
"to",
"value",
"input"
]
}
}'
Get Blob Data for the Optimism Chain
This example finds blob transactions used by the Optimism chain for data availability.
Key points:
- Starts at a relatively recent block (20,000,000)
- Filters for transactions from the Optimism sequencer address
- Specifically looks for type 3 (blob) transactions
- Results can be used to retrieve the actual blob data from Ethereum
curl --request POST \
--url https://eth.hypersync.xyz/query \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"from_block": 20000000,
"transactions": [
{
"from": ["0x6887246668a3b87F54DeB3b94Ba47a6f63F32985"],
"to": ["0xFF00000000000000000000000000000000000010"],
"type": [3]
}
],
"field_selection": {
"block": [
"number",
"timestamp",
"hash"
],
"transaction": [
"block_number",
"transaction_index",
"hash",
"from",
"to",
"type"
]
}
}'
Get Mint USDC Events
This example identifies USDC token minting events.
How it works:
- Filters for the USDC contract address
- Looks for Transfer events (topic0)
- Specifically matches when topic1 (from address) is the zero address, indicating a mint
- Returns detailed information about each mint event
curl --request POST \
--url https://eth.hypersync.xyz/query \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"from_block": 0,
"logs": [
{
"address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
"topics": [
[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
],
[
"0x0000000000000000000000000000000000000000000000000000000000000000"
],
[]
]
}
],
"field_selection": {
"block": [
"number",
"timestamp",
"hash"
],
"log": [
"block_number",
"log_index",
"transaction_index",
"data",
"address",
"topic0",
"topic1",
"topic2",
"topic3"
],
"transaction": [
"block_number",
"transaction_index",
"hash",
"from",
"to",
"value",
"input"
]
}
}'
Get All Transactions for an Address
This example retrieves all transactions where a specific address is either the sender or receiver.
Implementation notes:
- Starts from a specific block (15,362,000) for efficiency
- Uses two transaction filters in an OR relationship
- Only includes essential fields in the response
- Multiple queries may be needed for complete history
curl --request POST \
--url https://eth.hypersync.xyz/query \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"from_block": 15362000,
"transactions": [
{
"from": ["0xdb255746609baadd67ef44fc15b5e1d04befbca7"]
},
{
"to": ["0xdb255746609baadd67ef44fc15b5e1d04befbca7"]
}
],
"field_selection": {
"block": [
"number",
"timestamp",
"hash"
],
"transaction": [
"block_number",
"transaction_index",
"hash",
"from",
"to"
]
}
}'
Get Successful or Failed Transactions
This example shows how to filter transactions based on their status (successful or failed) for recent blocks.
How it works:
- First, query the current chain height
- Calculate a starting point (current height minus 10)
- Query transactions with status=1 (successful) or status=0 (failed)
# Get current height and calculate starting block
height=$((`curl https://eth.hypersync.xyz/height | jq .height` - 10))
# Query successful transactions (change status to 0 for failed transactions)
curl --request POST \
--url https://eth.hypersync.xyz/query \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data "{
\"from_block\": ${height},
\"transactions\": [
{
\"status\": 1
}
],
\"field_selection\": {
\"block\": [
\"number\",
\"timestamp\",
\"hash\"
],
\"transaction\": [
\"block_number\",
\"transaction_index\",
\"hash\",
\"from\",
\"to\"
]
}
}"
Api Tokens
File: api-tokens.mdx
API Tokens for HyperSync
Overview
API tokens provide authenticated access to HyperSync services, enabling enhanced capabilities and usage tracking.
HyperSync implements rate limits for requests without API tokens. API tokens will be required from 3 November 2025. Indexers deployed to Envio Cloud will have special access to HyperSync that does not require a custom API token.
Table of Contents
Generating API Tokens
You can generate API tokens through the Envio Dashboard:
- Visit https://envio.dev/app/api-tokens
- Sign in to your account (or create one if you don't have one)
- Follow the prompts to create a new token
- Copy and securely store your token
Implementation Guide
To use an API token, pass it as a bearer_token when creating a HyperSync client:
const client = new HypersyncClient({
url: "https://eth.hypersync.xyz",
apiToken: process.env.ENVIO_API_TOKEN!,
});
client = hypersync.HypersyncClient(hypersync.ClientConfig(
url="https://eth.hypersync.xyz",
bearer_token=os.environ.get("ENVIO_API_TOKEN")
))
let client = Client::new(ClientConfig {
api_token: std::env::var("ENVIO_API_TOKEN").expect("ENVIO_API_TOKEN must be set"),
..Default::default()
}).unwrap()
Understanding Usage
To understand your current month's usage, visit https://envio.dev/app/api-tokens. Usage is composed of two main components:
- Number of Requests: The total count of API requests made.
- Credits: A comprehensive calculation that takes into account multiple factors including data bandwidth, disk read operations, and other resource utilization metrics. This provides the most accurate representation of actual service usage. We're happy to provide more detailed breakdowns of the credit calculation upon request.
Security Best Practices
When working with API tokens:
- Never commit tokens to git repositories
- Use environment variables to store tokens instead of hardcoding
- Add token files like
.envto your `.gitignore - Rotate tokens periodically for enhanced security
- Limit token sharing to only those who require access
# Example .env file
ENVIO_API_TOKEN=your_secret_token_here
This approach keeps your tokens secure while making them available to your application at runtime.
Hypersync Supported Networks
File: hypersync-supported-networks.md
We are rapidly adding new supported networks. If you don't see your network here or would like us to add a network to HyperSync, pop us a message in our Discord.
The Tier is the level of support (and therefore reliability) based on the infrastructure running the chain. We are actively working to make the tier distinctions more clear and transparent to our users.
Currently, tiers relate to various service quality aspects including:
- Allocated resources and compute power
- Query processing speed
- Infrastructure redundancy
- Backup frequency and retention
- Multi-region availability
- Priority for upgrades and new features
- SLA guarantees
While detailed tier specifications are still being finalized, we're committed to providing transparent service level information in the near future.
If you are a network operator or user and would like improved service support or to discuss upgrading a chain's level of support, please reach out to us in Discord.
Notes:
- Base Traces*: Traces are available as a paid add-on. Start block: 24000000 (earlier blocks available on request)
- Eth Traces*: Traces are available as a paid add-on
- Injective*: Start block: 129846180 (non-evm before that)
- Sei*: Start block: 79123881 (non-evm before that)
- Sei Testnet*: Start block: 186100000 (non-evm before that)
Stream Config & Tuning
File: stream-config-tuning.md
When you stream (or collect) with a HyperSync client, the engine fans your block
range out across many concurrent HTTP requests, sizes each request automatically, and
delivers results to you in block order. StreamConfig controls that engine.
The defaults are good for most workloads, so you usually don't need to touch this.
Reach for tuning when you want to squeeze more throughput out of a specific workload, or to
bound memory. The fastest way to find a good config for your query is the
tune_stream tool, which sweeps configs against your
query and prints a comparison table.
Quick decision guide
| Your situation | Do this |
|---|---|
| Just getting started | Use the defaults. |
| Want maximum throughput on a busy contract (lots of logs) | Raise concurrency (for example 20). |
| Scanning a wide range for a rare event | Keep concurrency moderate (around 10); a larger batch_size helps. |
| Pulling full blocks + all transactions | Defaults are fine; the adaptive buffer handles the large responses. |
| Memory constrained | Set max_buffered_bytes to a fixed cap. |
| Hitting API rate limits | The client waits out limits and retries automatically. To read the quota yourself, see Inspecting rate limits; for higher limits, upgrade your plan. |
| Not sure | Run tune_stream against your query. |
How the engine works (the 30-second model)
Two ideas make every knob obvious:
- One HTTP request is one unit of work. The engine sizes each request to land near
response_bytes_targetbytes, based on the byte-density it has measured so far. If the server returns less than requested (a "truncation"), the leftover range becomes a gap that any free worker backfills in parallel. - Delivery is in block order. Results are buffered and handed to you contiguously, so
the stream yields one response per HTTP response in ascending (or
reverse) block order.
So concurrency sets how many requests run in parallel, and response_bytes_target sets how
big each response is. Because truncations are backfilled automatically, the engine is
forgiving: an over-estimated request size corrects itself rather than failing.
Configuration options
Field names below are shown in snake_case (Rust, Python). Node and TypeScript use
camelCase (for example response_bytes_target becomes responseBytesTarget).
| Option | Default | What it does |
|---|---|---|
concurrency | 10 | Number of requests in flight, and your main throughput knob. 0 is an error, 1 streams sequentially, 2 or more uses the parallel scheduler. |
response_bytes_target | 400_000 | Target size in bytes for each response. Each request is sized to aim here. Raise it for fewer, larger responses. |
batch_size | 1_000 | Initial block range for the first wave of requests, before any density has been measured. Also the fallback. |
min_batch_size | 200 | Lower clamp on the projected block count, to avoid tiny ranges. |
max_batch_size | unset | Optional hard cap on blocks per request. Unset means no cap (over-shoot self-corrects via backfill). Set it only if you specifically want to bound blocks per request. |
max_buffered_bytes | unset | Cap on bytes of fetched-but-undelivered data held for re-ordering (consumer backpressure). Unset means adaptive (it grows with the largest response seen) so byte-heavy pulls stay pipelined. Set a fixed value to bound memory. |
reverse | false | Stream from the top of the range downward. |
max_num_blocks, max_num_transactions, max_num_logs, max_num_traces | unset | Stop the stream once this many of an entity have been delivered. |
column_mapping, event_signature, hex_output | none | Output shaping (decoding, hex formatting). Not performance knobs. |
response_bytes_floor and response_bytes_ceiling were replaced by a single
response_bytes_target. max_batch_size became optional (unset means no cap).
max_buffered_bytes was added. If you set the old fields, switch to response_bytes_target.
Tuning recipes
These are good starting points. Confirm against your own query with
tune_stream.
Dense: busy contracts and all-logs
Lots of matching data per block. Throughput scales with parallelism.
# Python
config = hypersync.StreamConfig(concurrency=20, response_bytes_target=400_000)
// Node / TypeScript
const config = { concurrency: 20, responseBytesTarget: 400_000 };
// Rust has a ready-made preset
let config = StreamConfig::dense();
Sparse: rare events over a wide range
Most blocks match nothing. Raising concurrency past the default tends to just fragment the
empty range into more, smaller requests without adding throughput, so keep it moderate. A
larger batch_size lets the first wave cover more ground.
config = hypersync.StreamConfig(concurrency=10, batch_size=20_000)
const config = { concurrency: 10, batchSize: 20_000 };
let config = StreamConfig::sparse();
Archival: full blocks and all transactions
Each response is many megabytes, so the run is bound by the re-order buffer rather than
concurrency. Leave max_buffered_bytes unset so the adaptive buffer keeps the pipeline full.
config = hypersync.StreamConfig(concurrency=12)
const config = { concurrency: 12 };
let config = StreamConfig::archival();
Find the best config for your query
Rather than guess, measure. The Rust client ships a standalone tune_stream example
that runs your query under a grid of configs and prints a comparison table: throughput,
request count, truncation rate, and how close responses land to the target. It takes a query
as JSON, so it works for any query regardless of which client language you use.
1. Save your query as JSON
{
"from_block": 18000000,
"to_block": 18100000,
"logs": [
{ "topics": [["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]] }
],
"field_selection": { "log": ["block_number", "log_index", "data", "topic0"] }
}
2. Run the sweep
git clone https://github.com/enviodev/hypersync-client-rust
cd hypersync-client-rust
export ENVIO_API_TOKEN=<your-token>
export CHAIN_ID=1 # 1 = Ethereum mainnet
# Sweep a grid of configs and print a comparison table
cargo run -p tune_stream -- query.json
# Or a single detailed run with the default config
cargo run -p tune_stream -- query.json --single
The table shows, per config: requests, truncation %, blocks/s, MB/s, the mean size-vs-target ratio, and the observed buffer and in-flight counts. Pick the config with the best throughput for your workload.
3. Apply the winning config
Copy response_bytes_target, concurrency, and any other fields from the best row into your
client's StreamConfig.
Rust: attach an observer in your own code
Rust users can read the same metrics live, without the example, via the observer API:
use std::sync::Arc;
use hypersync_client::{Client, StreamConfig, StreamMetrics, StreamObserver};
let metrics = Arc::new(StreamMetrics::new());
let observer: Arc<dyn StreamObserver> = metrics.clone();
let mut rx = client
.stream_arrow_with_observer(query, StreamConfig::default(), observer)
.await?;
while let Some(res) = rx.recv().await {
let _ = res?; // consume the stream
}
let summary = metrics.summary();
println!(
"requests={} truncated={:.0}% blocks/s={:.0} mean size ratio={:.2}",
summary.num_requests,
summary.truncation_rate * 100.0,
summary.blocks_per_sec,
summary.mean_size_ratio,
);
The plain stream and stream_arrow methods do no metrics work; RequestStats are only
built when you attach an observer. (Surfacing this handle in the Node and Python clients is a
fast-follow; until then, use tune_stream for those languages.)
Reading the metrics
When tuning, the two numbers worth watching are:
- Mean size ratio, which is response size divided by
response_bytes_target. Around1.0means responses are landing on target. Well below1.0means the server is returning smaller responses than your target, which is perfectly normal for selective queries or minimal field selections; raisingresponse_bytes_targetwon't change it, and the way to go faster is usually moreconcurrency. - Blocks per second, which is the throughput you are tuning for.
Truncation is shown too, but you can usually ignore it. Some truncation is normal and harmless because the leftover range is backfilled automatically, and a sparse query that selects only a few fields will often show small, frequently-truncated responses while still streaming quickly. It is only worth a look if throughput is poor.
Rate limits
The clients handle rate limits for you: when the server signals a limit (HTTP 429) the client waits for the window to reset and retries, so a stream slows down rather than failing. If you want more headroom or higher throughput, upgrade your plan.
If you need the client to make fewer requests per unit time (for example to fit a fixed
request budget), concurrency is the lever. It trades throughput for request volume: fewer
requests run in parallel, so fewer go out per unit time. Setting concurrency = 1 streams
sequentially, one request at a time.
Inspecting rate limits from your code
Sometimes waiting is not enough: you may want to show quota in a dashboard, or coordinate a
budget across several processes that share one token. Every response carries the server's
x-ratelimit-* headers, and the clients expose them under the same names.
| Purpose | Rust | Node (EVM client) |
|---|---|---|
| Query and get the headers back | get_with_rate_limit, get_arrow_with_rate_limit | getWithRateLimit |
| Last observed headers, no request | rate_limit_info() | rateLimitInfo() |
| Wait out an exhausted window | wait_for_rate_limit() | waitForRateLimit() |
| Opt out of the automatic pre-request wait | ClientConfig::proactive_rate_limit_sleep | proactiveRateLimitSleep |
The query methods return a QueryResponseWithRateLimit, which pairs the normal response with
a RateLimitInfo:
| Field | Header | Meaning |
|---|---|---|
limit | x-ratelimit-limit | Total budget for the current window. |
remaining | x-ratelimit-remaining | Budget left in the window. |
reset_secs / resetSecs | x-ratelimit-reset | Seconds until the window resets. |
cost | x-ratelimit-cost | Budget consumed per request. |
Every field is optional, because parsing is best-effort: a missing or malformed header just
leaves it unset. Note that remaining counts budget units, not requests - divide it by
cost for the number of requests you have left (a limit of 50 with a cost of 10 is 5
requests per window).
proactive_rate_limit_sleep is on by default. With it enabled the client checks the last
observed headers before sending and waits out a window it knows is exhausted, instead of
spending a request on a certain 429. Turn it off if you are doing your own scheduling.
The names and shapes are identical, but the 429 behavior is not. On the EVM client
get_with_rate_limit does not retry a 429: it returns with no response body and the
rate-limit info filled in, and retrying is your job. On the Solana client the
*_with_rate_limit methods retry exactly like the plain get / get_arrow, so the response
is always present. The plain get methods retry on both.
Full default reference
concurrency = 10
response_bytes_target = 400_000
batch_size = 1_000
min_batch_size = 200
max_batch_size = unset (no cap)
max_buffered_bytes = unset (adaptive)
reverse = false
max_num_blocks / transactions / logs / traces = unset
Analyzing All Transactions To and From an Address
File: tutorial-address-transactions.md
Introduction
Understanding all transactions to and from an address is an interesting use case. Traditionally extracting this information would be very difficult with an RPC. In this tutorial, we'll introduce you to the evm-address-summary tool, which uses HyperSync to efficiently extract all transactions associated with a specific address.
About evm-address-summary
The evm-address-summary repository contains a collection of scripts designed to get activity related to an address. These scripts leverage HyperSync's efficient data access to make complex address analysis simple and quick.
GitHub Repository: https://github.com/enviodev/evm-address-summary
Available Scripts
The repository offers several specialized scripts:
-
All Transfers: This script scans the entire blockchain (from block 0 to the present) and retrieves all relevant transactions for the given address. It iterates through these transactions and sums up their values to calculate aggregates for each token.
-
NFT Holders: This script scans the entire blockchain and retrieves all token transfer events for an ERC721 address. It records all the owners of these tokens and how many tokens they have traded in the past.
-
ERC20 Transfers and Approvals: This script scans the blockchain and retrieves all ERC20 transfer and approval events for the given address.
It calculates the following:
- Token balances: Summing up all incoming and outgoing transfers for each token
- Token transaction counts: Counting the number of incoming and outgoing transactions for each token
- Approvals: Tracking approvals for each token, including the spender and approved amount
Quick Start Guide
Prerequisites
Basic Setup
-
Clone the Repository
git clone https://github.com/enviodev/evm-address-summary.git
cd evm-address-summary -
Install Dependencies
pnpm install -
Run a Script (example with all-transfers)
pnpm run all-transfers 0xYourAddressHere
For complete details on all available scripts, their usage, and example outputs, refer to the project README.
Customizing Network Endpoints
The scripts work with any network supported by HyperSync. To change networks, edit the hyperSyncEndpoint in the appropriate config file:
// For Ethereum Mainnet
export const hyperSyncEndpoint = "https://eth.hypersync.xyz";
For a complete list of supported networks, see our HyperSync Supported Networks documentation.
Practical Use Cases
One powerful application is measuring value at risk for any address, similar to revoke.cash. You can quickly scan an address to find all approvals and transfers to easily determine any outstanding approvals on any token. This helps identify potential security risks from forgotten token approvals.
Other use cases include:
- Portfolio tracking and analysis
- Auditing transaction history
- Research on token holder behavior
- Monitoring NFT ownership changes
Next Steps
- Check out the evm-address-summary repository for full documentation
- Explore the source code to understand how HyperSync is used for data retrieval
- Try modifying the scripts for your specific use cases
- Learn more about HyperSync's capabilities for blockchain data analysis
For any questions or support, join our Discord community or create an issue on the GitHub repository.
Troubleshooting
File: hypersync-troubleshooting.md
Common connectivity issues with HyperSync and HyperRPC endpoints. If nothing here helps, ask in our Discord.
DNS resolution failures
Symptoms: requests hang or time out intermittently, the first request after idle is slow but retries work, or you see Could not resolve host / SERVFAIL for *.hypersync.xyz or *.rpc.hypersync.xyz.
Cause: HyperSync endpoints use geographic load balancing with a multi-step DNS delegation chain and short TTLs. Some ISP and home-router resolvers (notably in South Africa and parts of Asia) can't follow the chain and return SERVFAIL. This is a client-side resolver issue, not an outage; public resolvers like Cloudflare and Google resolve it correctly.
Verify:
dig eth.hypersync.xyz A # your system/ISP resolver
dig eth.hypersync.xyz A @1.1.1.1 # Cloudflare
dig eth.hypersync.xyz A @8.8.8.8 # Google
If the first returns SERVFAIL and the others return NOERROR with IPs, your resolver is the problem.
Fix: switch your system DNS to public resolvers.
-
Linux (systemd-resolved):
sudo mkdir -p /etc/systemd/resolved.conf.d
printf '[Resolve]\nDNS=1.1.1.1 8.8.8.8 1.0.0.1 8.8.4.4\nFallbackDNS=9.9.9.9\n' \
| sudo tee /etc/systemd/resolved.conf.d/dns.conf
sudo systemctl restart systemd-resolvedRevert by deleting the file and restarting
systemd-resolved. -
macOS:
sudo networksetup -setdnsservers Wi-Fi 1.1.1.1 8.8.8.8 1.0.0.1 8.8.4.4(replaceWi-Fiwith your interface; revert with... Wi-Fi Empty). -
Windows: Settings > Network & Internet > Wi-Fi > Hardware properties > DNS server assignment > Edit > Manual. Preferred
1.1.1.1, alternate8.8.8.8. -
Docker: add
dns: ["1.1.1.1", "8.8.8.8"]to the service in your compose file, or pass--dns 1.1.1.1 --dns 8.8.8.8todocker run.
| Resolver | IPs |
|---|---|
| Cloudflare | 1.1.1.1, 1.0.0.1 |
8.8.8.8, 8.8.4.4 | |
| Quad9 | 9.9.9.9 |
Connection timeouts
If DNS resolves but requests still hang, check the endpoint directly:
curl -v --max-time 10 https://eth.hypersync.xyz/height
If this returns a block height, the service is healthy and the problem is on the network path (typically a corporate/university firewall or transient regional routing). Try from a different network to confirm.
Getting help
Share the output of the dig and curl commands above, plus your location and ISP, in our Discord.
Solana HyperSync
File: Solana/solana.md
Solana HyperSync is a query API over Solana history. One endpoint,
https://solana.hypersync.xyz, serves slots, transactions, instruction
calls, logs, account activity (SOL + SPL token) and rewards - filtered
server-side and returned as columns you choose, in JSON or Apache Arrow. Query
it with the Solana client or any HTTP client.
First query: everything an address touched
Filters run server-side, so what costs an RPC node a getSignaturesForAddress call plus one getTransaction per signature is one request here, returning only the columns you ask for:
export TOKEN="your-api-token" # see https://envio.dev/app/api-tokens
HEAD=$(curl -sS https://solana.hypersync.xyz/height)
curl -sS "https://solana.hypersync.xyz/query" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from_slot": '$((HEAD - 10000))',
"to_slot": '$HEAD',
"field_selection": {
"account_activity": ["slot", "transaction_id", "account", "pre_balance", "post_balance", "mint", "pre_token_balance", "post_token_balance"]
},
"account_activity": [
{ "account": ["MfDuWeqSHEqTFVYZ7LoexgAK9dxk7cy4DFJWjWMGVWa"] },
{ "owner": ["MfDuWeqSHEqTFVYZ7LoexgAK9dxk7cy4DFJWjWMGVWa"] }
]
}'
Each row is one account's before/after balance in one transaction, and transaction_id is the Solana signature. SOL and SPL token movements share the account_activity table; the two selections are OR-ed because account is the wallet on a SOL row but the token account on a token row, so owner catches the token side. next_slot tells you where to resume.
Swap the filter, keep the shape. The same request filters on a program and discriminator (instruction_calls), a fee payer or signature (transactions), a log kind (logs), or a mint or owner (account_activity) - AND-ed within one selection object, OR-ed across several.
See every available filter →, grab a ready-made query from curl Examples, or build one by clicking in the Query Builder.
History starts at the earliest slot we have indexed rather than at genesis - mainnet is around slot 403,000,000 as of September 2026 - and we keep extending it backwards as we backfill deeper. A range that straddles that slot is served from it onward; one entirely below it comes back empty rather than erroring, with next_slot not advancing. Need history further back? Tell us on Discord.
You'll need an API token. Every endpoint requires a Bearer token except GET /height and GET /height/sse. Generate one at https://envio.dev/app/api-tokens — see API tokens for details.
Slots vs blocks: some slots have no block (skipped leader, etc.), so a query over [from_slot, to_slot) can return fewer block rows than the slot span implies.
Differences vs EVM HyperSync
| Concept | EVM | Solana |
|---|---|---|
| Unit of progress | block | slot |
| Range bounds | from_block / to_block | from_slot / to_slot |
| Primary filter | logs, transactions, traces | instruction_calls, transactions, logs, account_activity |
| Match key | event topic + address | program ID + discriminator + account positions |
| Logs | Contract events (topics + structured data) | Program output lines (free-form strings; filter by emitter program_id and parsed kind) |
| Pagination | next_block | next_slot |
Endpoints
| Path | Description |
|---|---|
POST /query | JSON query, JSON response. |
POST /query/arrow | Same query; response is Apache Arrow IPC (smaller and faster to decode than JSON). |
GET /height | Current synced slot. |
GET /height/sse | Server-sent events stream of the head slot (see curl Examples). |
GET /health | Health check. |
POST /, POST /rpc | Solana JSON-RPC-compatible facade for tooling that already speaks JSON-RPC. Method coverage is partial, so prefer POST /query for indexing. |
What's coming next
- Deeper history. The earliest indexed slot keeps moving earlier; backfill depth is prioritized by demand.
- Decoded, higher-level data on top of the raw tables: IDL-aware decoding and shortcuts for common programs.
- Wider JSON-RPC coverage on the compatibility facade (
POST //POST /rpc), for tooling that already speaks JSON-RPC. - More clients. Today there's the Rust client; Node bindings are next, Python after that.
Want one of these sooner, or hit a missing field or a filter you wish existed? Tell us on Discord or file it on GitHub. Share a sample transaction signature or program ID and we'll map it to a concrete query path - the roadmap here is driven by the use cases people bring us.
Query & Response
File: Solana/solana-query.md
A query selects a slot range, optional filters on instruction calls / transactions / logs / account activity, and the columns you want. The server returns matched rows plus a next_slot cursor. Some slots have no block, so blocks can be sparse across the requested range.
Query shape
{
"from_slot": 391800000,
"to_slot": 391800100,
"include_all_blocks": false,
"instruction_calls": [ ... ],
"transactions": [ ... ],
"logs": [ ... ],
"account_activity": [ ... ],
"field_selection": { ... }
}
from_slotis inclusive,to_slotis exclusive. Omitto_slotto run toward the current head.- Within one selection object, all set fields are AND-ed. Multiple objects within one array are OR-ed. Different arrays are AND-ed against each other (see the warning below).
- If every selection array is absent or empty, the query matches every slot in range: all block rows, plus their
account_activityandrewardrows, come back one slot at a time (even whenfield_selectionnames onlyblock). This is heavy; keep the range small.include_all_blocks: truereturns every block header in range even when filters are set (by default only blocks with a match are returned).
Selections in different arrays are intersected. Adding a transactions filter next to an instruction_calls filter returns only the instructions whose transaction also matches; a selection that matches nothing zeroes every table in the response, with no error or warning.
Measured on slot 437500000:
| Query | Instruction rows |
|---|---|
instruction_calls: [{executing_account: [Tokenkeg...], d1: ["03"]}] alone | 44 |
transactions: [{fee_payer: [P]}] alone, where P paid for one of those transactions | 16 |
| both together | 2 |
the instruction filter plus transactions: [{fee_payer: ["1111...1111"]}] (matches nothing) | 0 |
For a union, send separate queries and merge. This is about which rows match; whether a matched row's related rows come back is decided by field_selection (see Join behavior).
The top-level query object and field_selection are strict: an unrecognized key (a removed table such as balances / token_balances, the removed include_account_activity flag, or a misspelled max_num_*) fails the whole query rather than silently widening it. Keys inside a selection object are still ignored if unknown. To request every activity row in a range, use an empty selection: "account_activity": [{}].
Several keys were renamed; legacy names are still accepted on input, but responses use the new names. See Renamed fields and compatibility.
Filters
InstructionSelection
Selects instruction calls (a single program invocation, including inner CPIs). Array key: instruction_calls (legacy: instructions).
| Field | Description |
|---|---|
executing_account | Match the invoked program (base58 pubkeys). Legacy: program_id. |
d1 / d2 / d4 / d8 | First N bytes of instruction data as hex; 0x prefix optional ("0x03" and "03" are equivalent). |
a0 - a9 | Account pubkey at that index in the instruction's account metas (a0 = first). Which index is "the mint", "the pool", etc. is defined by the program's IDL, not by Solana globally. |
is_inner | true = inner only, false = outer only, omitted = both. |
tx_success | true = instructions of successful transactions only, false = failed only, omitted = both. Legacy: is_committed. |
tx_success is the success of the parent transaction, applied to every instruction call of that transaction (Solana metadata only records instructions that actually ran). Set "tx_success": true to drop failed transactions server-side. "tx_success": false can legitimately match nothing: servers running the failed-transaction trim keep no instruction rows for failed transactions; the transactions themselves are still served (with err and fee) via the transactions table's success filter.
TransactionSelection
| Field | Description |
|---|---|
fee_payer | Match fee payer pubkey. |
transaction_id | Match by signatures[0] (base58), the canonical Solana transaction signature. |
transaction_index | Match by transaction_index. See transaction_index semantics. |
success | true = succeeded only, false = failed only, omitted = both. |
LogSelection
| Field | Description |
|---|---|
program_id | Match log emitter program. |
kind | Parsed log line category (below). |
kind | Typical meaning |
|---|---|
invoke | Program <id> invoke <depth> |
success | Program <id> success |
failed | Program <id> failed: ... |
consumed | Program <id> consumed <n> of <m> compute units |
log | Program log: ... |
data | Program data: <base64> |
other | Anything else (full text still in message) |
An unknown kind in a filter is an error; on the response side an unrecognized value decodes as other. Not every range carries the invoke / success / failed / consumed lines: SQD-ingested and default RPC-ingested ranges only carry log / data / other, so do not assume every invocation has an invoke row.
AccountActivitySelection
Selects rows of the unified account_activity table (native SOL and SPL token movements). An empty selection {} matches every row in range without forcing every block into the response.
| Field | Description |
|---|---|
kind | "native" (the rows the old balances table held), "token" (the old token_balances rows), or omitted for both. |
account | Match by account address: the token account on a token row, the wallet on a native row. |
transaction_id | Match by the transaction's base58 signatures[0]. |
mint | Match by mint. Only token rows carry a mint, so this restricts to token activity. |
owner | Match by owner (wallet). The stored column is split into pre_owner / post_owner; this filter matches either side, so an in-transaction SetAuthority(AccountOwner) change still matches. |
program_id | Match by token program id (SPL Token vs Token-2022), pre or post. |
is_signer / is_writable / is_fee_payer / from_lookup_table | Header-derived position flags. A null flag matches neither true nor false. |
Because account means different things on the two sides, "everything for wallet W" is two selections: [{ "account": ["W"] }, { "owner": ["W"] }].
Field selection
field_selection chooses columns per table. Omit a table key to receive all columns for that table.
{
"field_selection": {
"block": ["slot", "blockhash", "block_time"],
"instruction_call": ["slot", "executing_account", "data", "d8"],
"transaction": ["slot", "fee_payer", "success"]
}
}
Available fields (by table)
| Table | Fields |
|---|---|
block | slot, blockhash, parent_slot, parent_blockhash, block_time, block_height |
transaction | slot, transaction_index, transaction_id, signatures, fee_payer, success, err, fee, compute_units_consumed, account_keys, recent_blockhash, version, loaded_addresses_writable, loaded_addresses_readonly, has_dropped_log_messages |
instruction_call | slot, transaction_index, instruction_address, executing_account, executing_account_index, account_arguments, account_index_arguments, data, d1, d2, d4, d8, a0-a9, is_inner, tx_success, error, compute_units_consumed |
log | slot, transaction_index, instruction_address, program_id, kind, message |
account_activity | slot, transaction_index, transaction_id, account_index, account, pre_balance, post_balance, is_signer, is_writable, is_fee_payer, from_lookup_table, mint, pre_owner, post_owner, token_decimals, pre_token_balance, post_token_balance, pre_program_id, post_program_id, token_state |
reward | slot, pubkey, lamports, post_balance, reward_type, commission |
Field notes:
tx_success(instruction call): see InstructionSelection. Legacy column name:is_committed.instruction_address(instruction call, log): where the instruction sits in the transaction:[2]= third top-level instruction;[2, 0]= first inner instruction inside it.data/d1-d8(instruction call): hex-encoded instruction bytes, without a0xprefix in responses even if your filter used one.executing_account_index/account_index_arguments(instruction call): positions of the executing account and account arguments in the transaction's resolved key list (account_keys++ ALT writable ++ ALT readonly). Null when the source could not resolve positions.error/compute_units_consumed(instruction call): per-invocation failure reason (e.g."custom program error: 0x1") and compute units. SQD serves both directly; RPC and Firehose ranges derive them from theProgram <id> failed/consumedlog lines, so both are null where the source did not record them.has_dropped_log_messages(transaction):truewhen the validator truncated this transaction's logs, so itslogsrows are incomplete. Null means the source could not say.transaction_id(transaction) andtoken_state(account activity) are computed at serving time but selected like any other field.token_stateisnot_a_token,opened(token account created in this transaction),closed, orpersisted; select it instead of inferring "is this a token row" from nullmint.
Value types in responses
- Every response field is optional. A missing value means "not selected, or the source could not supply it", never zero or false.
- Addresses, hashes and signatures are base58 strings, parsed strictly (32 bytes for a pubkey/blockhash, 64 for a signature). A malformed value in a filter is an error, not a filter that silently matches nothing.
- Token balances are strings.
pre_token_balance/post_token_balanceare raw base units (scale bytoken_decimals) as decimal strings, so JavaScript consumers don't lose precision above 2^53. Raw SPL amounts areu64on-chain in both SPL Token and Token-2022. Lamport fields (pre_balance/post_balance,fee) stay numeric.
transaction_index semantics
transaction_index is a dense 0..n rank over the stored, non-vote transactions of a slot, in block order. It is not the transaction's original position in the block: vote transactions are excluded at ingest, and every source is renumbered onto the same key. (slot, transaction_index) is the join key tying instruction_calls, logs, and account_activity rows to their transaction; do not compare it against an index from an RPC getBlock response.
The account_activity table
account_activity replaces the old balance and token_balance tables. Each row is one account's activity in one transaction:
- Native side (
pre_balance,post_balance, lamports): populated when the account's SOL balance changed, null otherwise. - Token side (
mint,pre_owner,post_owner,token_decimals,pre_token_balance,post_token_balance,pre_program_id,post_program_id): populated when the account appears in the transaction's token-balance metadata, null otherwise.
A row commonly carries both sides, since a token account also holds lamports; the two are independent axes (for wrapped SOL, lamports equal the token amount plus the rent-exempt reserve). A null pre_owner means the token account was opened in this transaction, a null post_owner that it was closed (same convention for pre_program_id / post_program_id). account_index is the account's position in the transaction's resolved key list; the flags (is_signer, is_writable, is_fee_payer, from_lookup_table) come from the message header and are null where a source could not supply them.
Join behavior
The server joins related rows based on which tables you include in field_selection: filter on instruction_calls and also select transaction fields, and you get the parent transaction for each matched instruction. There is currently a single default join mode; finer control (matched rows only, or all rows of matched transactions) is planned. Tell us on Discord or GitHub if you need it.
Limits (optional)
Approximate server-side caps on rows returned per table: max_num_blocks, max_num_transactions, max_num_instructions (this key keeps the legacy noun; max_num_instruction_calls is rejected), max_num_logs, max_num_account_activity. Defaults are usually fine.
Response
Top-level keys: next_slot, total_execution_time_ms, optional rollback_guard, and one key per table when present: blocks, transactions, instruction_calls, logs, account_activity, rewards.
instruction_calls is [[row, row, ...], ...], the same framing EVM HyperSync uses for data. A single response is usually one batch, so table[0] looks like it works until it hands you a batch instead of a row. Flatten first: in jq, [.instruction_calls[][]], and the row count is [.instruction_calls[][]] | length. Batches carry no meaning of their own (they do not correspond to blocks or transactions).
{
"next_slot": 391800050,
"total_execution_time_ms": 12,
"rollback_guard": null,
"blocks": [[{ "slot": 391800000, "blockhash": "8dK...", "block_time": 1731000123 }]],
"instruction_calls": [[{
"slot": 391800000,
"executing_account": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"account_arguments": ["7xK...", "9mY...", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
"data": "33e685a4017f83ad2e0f27c68d080000c61cf79a00000000",
"tx_success": true
}]],
"transactions": []
}
Instruction data is the raw instruction bytes as hex; for filtering, use the discriminator fields (d1 / d2 / d4 / d8). On failure, transaction.err is the chain's error structure as a JSON-encoded string (e.g. "{\"InstructionError\":[2,{\"Custom\":2}]}"), so parse it before inspecting it.
Pagination
Use the response's next_slot as the next request's from_slot.
- Bounded scan (
to_slotset): repeat whilenext_slot < to_slot. - To head (no
to_slot): repeat whilenext_slotstrictly increases. If it doesn't, you've caught up to the head or hit a limit; back off.
The server may stop early on a time or size budget, so a single response can cover more or fewer slots than requested.
Reorg detection (rollback_guard)
rollback_guard describes the server's current in-memory head window, not the slots in the response: slot_number / blockhash are the last slot in the window and its hash, first_slot_number / first_previous_blockhash the first slot in the window and its parent hash. It is present on most responses, including ones far behind head, and null when the server has no complete window to describe. The shape mirrors the EVM RollbackGuard with Solana naming.
{
"slot_number": 391800099,
"timestamp": 1731000000,
"blockhash": "8dK...",
"first_slot_number": 391800000,
"first_previous_blockhash": "3nF..."
}
Use it to detect a shallow reorg before committing near-head data: if you have already ingested first_slot_number - 1 or slot_number, compare the blockhashes you stored for them against first_previous_blockhash / blockhash. A mismatch means what you ingested is no longer on the server's head fork; re-sync from a finalized slot or the newest slot whose hash still agrees. The window moves between calls, so do not compare one page's guard against the next page's, and never treat slot numbers alone as stable identifiers near head; reconcile with blockhash / parent_blockhash.
Renamed fields and compatibility
Legacy names are still accepted on input, but responses use the new names.
| Location | Legacy name | Current name |
|---|---|---|
| Top-level query | instructions | instruction_calls |
InstructionSelection | program_id | executing_account |
InstructionSelection | is_committed | tx_success |
field_selection | instruction | instruction_call |
instruction_call field | program_id | executing_account |
instruction_call field | accounts | account_arguments |
instruction_call field | is_committed | tx_success |
Removed, not renamed (each is a loud error because the envelope rejects unknown keys):
- The
balance/token_balancefield-selection tables and the top-levelbalances/token_balancesselections: useaccount_activity. - The top-level
include_account_activityflag: use"account_activity": [{}]. - The
account_activity.ownercolumn, split intopre_owner/post_owner. Theownerfilter is unchanged and matches either side.
Authentication
Every endpoint requires a Bearer token except GET /height and GET /height/sse. See API tokens for how to generate one.
Solana Client
File: Solana/solana-client.md
hypersync-client-solana is the Rust client for Solana HyperSync. It speaks the
Arrow endpoint (POST /query/arrow), retries transient failures, waits out rate limits, and
paginates a slot range across many concurrent requests for you.
Install
[dependencies]
hypersync-client-solana = "0.2"
tokio = { version = "1", features = ["full"] }
Quick start
use std::sync::Arc;
use hypersync_client_solana::{config::ClientConfig, Client};
use hypersync_solana_net_types::query::SolanaQuery;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Arc::new(Client::new(ClientConfig {
url: "https://solana.hypersync.xyz".into(),
bearer_token: std::env::var("HYPERSYNC_BEARER_TOKEN").ok(),
..Default::default()
})?);
let height = client.get_height().await?;
let query = SolanaQuery {
from_slot: height.saturating_sub(100),
to_slot: Some(height),
include_all_blocks: true,
..Default::default()
};
let resp = client.get(&query).await?;
println!("{} blocks, next_slot {}", resp.blocks.len(), resp.next_slot);
Ok(())
}
ClientConfig fields: url, bearer_token, http_req_timeout (30s), max_num_retries
(12), retry_base_ms (500), retry_ceiling_ms (5000), and
proactive_rate_limit_sleep (true). See API tokens for the
bearer token.
Typed rows or Arrow
Every method comes in two flavors: typed structs, or the raw Arrow record batches the server sent.
| What you want | Single query | Whole slot range |
|---|---|---|
| Typed structs | get | collect |
| Arrow record batches | get_arrow | collect_arrow |
The typed structs live in hypersync_client_solana::simple_types: Block, Transaction,
InstructionCall, Log, AccountActivity, Reward, bundled into a SolanaResponse with one
Vec per table. Arrow responses instead carry data.tables, a map keyed by table name
(blocks, transactions, instruction_calls, logs, account_activity, rewards).
Use Arrow when feeding a columnar pipeline (Polars, DataFusion, Parquet); use typed structs for ordinary application code.
Option<T>field_selection can project any column away, so a None means exactly "not selected, or the
source could not supply it" - never zero or false. Addresses, hashes, and signatures are the
base58 newtypes Address, Hash, and Signature, which parse strictly and reject a malformed
value loudly rather than matching nothing. InstructionCall::stack_height() is a convenience
view over instruction_address (its length, matching Solana's native stack height).
Streaming a range
collect and collect_arrow fan a slot range out across concurrent requests and merge the
results; stream_arrow gives you the same engine but yields each response as it arrives, in
slot order, through an mpsc receiver.
use hypersync_client_solana::config::StreamConfig;
let resp = client
.collect(query, StreamConfig::default())
.await?;
StreamConfig for the Solana client:
| Option | Default | What it does |
|---|---|---|
concurrency | 10 | Requests in flight. The main throughput knob, and the lever for making fewer requests per unit time. |
batch_size | 1_000 | Slots per chunk before any response size has been measured. |
min_batch_size | 100 | Lower clamp on the adaptive chunk size. |
max_batch_size | 200_000 | Upper clamp on the adaptive chunk size. |
response_bytes_ceiling | 500_000 | Responses above this shrink the next chunk. |
response_bytes_floor | 250_000 | Responses below this grow the next chunk. |
These are Solana-specific names: the EVM client's StreamConfig targets a single
response_bytes_target instead of a floor/ceiling pair, so the
tuning guide transfers as advice but not field for
field.
Pagination and reorgs
A single get covers as much of the range as the server's budget allows, so use the response's
next_slot as the next request's from_slot. collect and stream_arrow do this for you.
Responses can carry a rollback_guard describing the server's in-memory head window, so you
can detect a shallow reorg before committing near-head data. It is absent when the server has
no complete window to describe, and on a paginated collect it is the guard of the last page
that carried one.
See Reorg detection for the algorithm.
Rate limits
The client waits out rate limits and retries, so a stream slows down rather than failing. To
read the quota yourself, the Solana client exposes the same surface as the EVM client:
get_with_rate_limit / get_arrow_with_rate_limit, rate_limit_info(),
wait_for_rate_limit(), and the proactive_rate_limit_sleep config field. See
Inspecting rate limits from your code
for the fields, the header mapping, and the one behavioral difference from the EVM client
(the Solana *_with_rate_limit methods retry a 429; the EVM ones do not).
Node bindings
The repository also contains napi-rs Node bindings (node/), exposing SolanaClient with
getHeight(), query(), getWithRateLimit(), rateLimitInfo(), and waitForRateLimit().
They are not published to npm yet, so build them from source
(repo); the streaming methods are Rust
only for now. If you use them:
- The query object is camelCase (
fromSlot,instructionCalls,executingAccount,fieldSelection), butfieldSelectionvalues are the snake_case column names from Available fields, for example{ instructionCall: ["executing_account", "tx_success"] }. response.tablesis keyed by table name, so instruction rows are underinstruction_calls.includeAccountActivityis deprecated; setting it totruethrows with guidance. UseaccountActivity: [{}].
Upgrading to 0.2.0
0.2.0 locked the query API. The renames are breaking on the response side; the request
side still accepts legacy names as aliases. Full mapping (including is_committed to
tx_success and the account_activity.owner split into pre_owner / post_owner):
Renamed fields and compatibility.
Solana curl Examples
File: Solana/solana-curl-examples.md
Copy-paste examples against https://solana.hypersync.xyz. You'll need an API token: generate one at https://envio.dev/app/api-tokens (see API tokens) and pass it via Authorization: Bearer <token>; GET /height and /height/sse work without one.
curl is great for testing; for production, prefer the Solana client, which uses Arrow and handles pagination, retries, and rate limits for you.
export URL=https://solana.hypersync.xyz
export TOKEN="your-api-token"
# Every example runs over a short range just below the head
HEAD=$(curl -sS "$URL/height")
FROM=$((HEAD - 2000))
TO=$((HEAD - 1900))
# JSON POST helper (adds auth + content-type)
curl_query() {
curl -sS "$URL/query" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$1"
}
The examples splice $FROM / $TO into single-quoted JSON with '$FROM'; a range below the earliest indexed slot returns empty tables with next_slot not advancing.
Discriminator filters accept hex with or without a 0x prefix. Every response table is an array of row batches, so the jq below flattens with [.table[][]] before indexing or counting (see Response).
Prefer clicking over typing? The HyperSync Query Builder builds and runs these same queries visually — several of the patterns below have a one-click Quick start template (linked inline); paste in your own token before executing.
Quick checks
curl -sS "$URL/height"
curl -sS -H "Authorization: Bearer $TOKEN" "$URL/health"
Head slot (SSE)
curl -sSN -H "Accept: text/event-stream" "$URL/height/sse"
Orca Whirlpool (swap discriminator)
8-byte Anchor discriminator. Example response shape (truncated):
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": {
"instruction_call": ["slot", "transaction_index", "executing_account", "account_arguments", "data", "d8"],
"transaction": ["slot", "signatures", "fee_payer", "success", "fee"]
},
"instruction_calls": [{
"executing_account": ["whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"],
"d8": ["0xf8c69e91e17587c8"]
}]
}' | jq '{next_slot, sample_instruction: [.instruction_calls[][]][0], sample_tx: [.transactions[][]][0]}'
Try it: Query Builder → Quick start → Whirlpool Swaps (d8 Anchor).
SPL Token Transfer (d1)
1-byte discriminator: 0x03 = Transfer (hex with or without 0x). Token movements come from the unified account_activity table (pre_token_balance / post_token_balance are raw base-unit decimal strings).
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": {
"instruction_call": ["slot", "executing_account", "account_arguments", "data", "d1"],
"account_activity": ["slot", "transaction_index", "account", "mint", "pre_owner", "post_owner", "token_state", "pre_token_balance", "post_token_balance"]
},
"instruction_calls": [{
"executing_account": ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
"d1": ["0x03"]
}]
}'
Try it: Query Builder → Quick start → SPL Token Transfers (d1).
Wallet activity (native SOL + tokens)
account is the wallet on a native row and the token account on a token row, and fields within one selection are AND-ed, so "everything for wallet W" is two selections.
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": {
"account_activity": ["slot", "transaction_id", "account", "pre_balance", "post_balance", "mint", "pre_owner", "post_owner", "token_state", "pre_token_balance", "post_token_balance"]
},
"account_activity": [
{ "account": ["MfDuWeqSHEqTFVYZ7LoexgAK9dxk7cy4DFJWjWMGVWa"] },
{ "owner": ["MfDuWeqSHEqTFVYZ7LoexgAK9dxk7cy4DFJWjWMGVWa"] }
]
}'
Try it: Query Builder → Quick start → Wallet Activity (account OR owner).
To pull every activity row in a range (the replacement for the removed include_account_activity flag), use one empty selection:
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": { "account_activity": ["slot", "account", "pre_balance", "post_balance"] },
"account_activity": [{}]
}'
Try it: Query Builder → Quick start → All Account Activity.
Successful transactions only
tx_success filters instruction calls by the success of their parent transaction, server-side.
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": {
"instruction_call": ["slot", "executing_account", "d8", "tx_success", "error", "compute_units_consumed"]
},
"instruction_calls": [{
"executing_account": ["whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"],
"tx_success": true
}]
}'
Jupiter or Orca (program-only OR)
Each object in instruction_calls is OR-ed; this is the "match by program only" pattern (no discriminator).
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": {
"instruction_call": ["slot", "executing_account", "data", "d8"]
},
"instruction_calls": [
{ "executing_account": ["JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"] },
{ "executing_account": ["whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"] }
]
}'
Try it: Query Builder → Quick start → Jupiter OR Orca (multi-filter OR).
Transactions by fee payer
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": {
"transaction": ["slot", "signatures", "fee_payer", "success", "fee", "compute_units_consumed"],
"instruction_call": ["slot", "executing_account", "data", "account_arguments"]
},
"transactions": [{
"fee_payer": ["MfDuWeqSHEqTFVYZ7LoexgAK9dxk7cy4DFJWjWMGVWa"]
}]
}'
Try it: Query Builder → Quick start → Txns by Fee Payer.
Pump.fun bonding-curve trades (account index)
a2 matches the third account in the instruction's account metas (a0 = first). For Pump.fun's buy/sell instructions the mint is account index 2 per that program's IDL, not a Solana-wide rule.
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": {
"instruction_call": ["slot", "executing_account", "account_arguments", "data", "d8", "a2"],
"transaction": ["slot", "fee_payer", "success"]
},
"instruction_calls": [{
"executing_account": ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"]
}]
}'
Raydium AMM logs
curl_query '{
"from_slot": '$FROM',
"to_slot": '$TO',
"field_selection": {
"log": ["slot", "program_id", "kind", "message"],
"transaction": ["slot", "fee_payer", "success"]
},
"logs": [{
"program_id": ["675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"]
}]
}'
Paginating a bounded scan
Use the same termination rule as Query & Response: stop when next_slot >= to_slot, or when next_slot does not advance (stuck at head).
SLOT=$FROM
while [ "$SLOT" -lt "$TO" ]; do
RESP=$(curl_query "{
\"from_slot\": $SLOT,
\"to_slot\": $TO,
\"field_selection\": { \"instruction_call\": [\"slot\", \"executing_account\", \"d8\"] },
\"instruction_calls\": [{ \"executing_account\": [\"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc\"] }]
}")
echo "$RESP" | jq '([.instruction_calls[][]] | length), .next_slot'
NEXT=$(echo "$RESP" | jq -r .next_slot)
if [ "$NEXT" -ge "$TO" ] || [ "$NEXT" -le "$SLOT" ]; then
break
fi
SLOT=$NEXT
done
Hyperfuel
File: HyperFuel/hyperfuel.md
HyperSync is a high-performance data node and accelerated data query layer that powers Envio’s Indexing framework, HyperIndex, for up to 1000x faster data retrieval than standard RPC methods.
HyperFuel is HyperSync adapted for the Fuel Network and is exposed as a low-level API for developers and data analysts to create flexible, high-speed queries for all fuel data.
Users can interact with the HyperFuel in Rust, Python, NodeJS clients, or directly via the JSON API to extract data into parquet files, arrow format, or as typed data. Client examples are listed furhter below.
Using HyperFuel, application developers can easily sync and search large datasets in a few minutes. HyperFuel is an ideal solution for indexers, block explorers, data analysts, bridges, and other applications or use cases focused on performance.
You can integrate with HyperFuel using any of our clients:
- Rust: https://github.com/enviodev/hyperfuel-client-rust
- Python: https://github.com/enviodev/hyperfuel-client-python
- Nodejs: https://github.com/enviodev/hyperfuel-client-node
- JSON API: https://github.com/enviodev/hyperfuel-json-api
HyperFuel supports Fuel mainnet and testnet: Mainnet: https://fuel.hypersync.xyz Testnet: https://fuel-testnet.hypersync.xyz
HyperFuel requires an API token. Generate one from the Envio Dashboard and pass it to the client when you initialize it. For example, the Rust client below reads it from the ENVIO_API_TOKEN environment variable and passes it via the api_token config field. See API Tokens for more details.
Example usage
Below is an example of a Hyperfuel query in each of our clients searching the first 1,300,000 blocks for all input objects of a specific asset-id. This example returns 10,543 inputs in around 100ms - not including latency.
Rust (repo)
use hyperfuel_client::{Client, ClientConfig};
use hyperfuel_net_types::Query;
use url::Url;
#[tokio::main]
async fn main() {
let client_config = ClientConfig {
url: Some(Url::parse("https://fuel-testnet.hypersync.xyz").unwrap()),
api_token: std::env::var("ENVIO_API_TOKEN")
.expect("ENVIO_API_TOKEN env var is required, get a token from https://envio.dev/app/api-tokens"),
..Default::default()
};
let client = Client::new(client_config).unwrap();
// Construct query in json. Can also construct it as a typed struct (see predicate-root example)
let query: Query = serde_json::from_value(serde_json::json!({
// start query from block 0
"from_block": 0,
// if to_block is not set, query runs to the end of the chain
"to_block": 1300000,
// load inputs that have `asset_id` = 0x2a0d0ed9d2217ec7f32dcd9a1902ce2a66d68437aeff84e3a3cc8bebee0d2eea
"inputs": [
{
"asset_id": ["0x2a0d0ed9d2217ec7f32dcd9a1902ce2a66d68437aeff84e3a3cc8bebee0d2eea"]
}
],
// fields we want returned from loaded inputs
"field_selection": {
"input": [
"tx_id",
"block_height",
"input_type",
"utxo_id",
"owner",
"amount",
"asset_id"
]
}
}))
.unwrap();
let res = client.get_selected_data(&query).await.unwrap();
println!("inputs: {:?}", res.data.inputs);
}
Python (repo)
import hyperfuel
from hyperfuel import InputField
import asyncio
async def main():
client = hyperfuel.HyperfuelClient()
query = hyperfuel.Query(
# start query from block 0
from_block=0,
# if to_block is not set, query runs to the end of the chain
to_block = 1300000,
# load inputs that have `asset_id` = 0x2a0d0ed9d2217ec7f32dcd9a1902ce2a66d68437aeff84e3a3cc8bebee0d2eea
inputs=[
hyperfuel.InputSelection(
asset_id=["0x2a0d0ed9d2217ec7f32dcd9a1902ce2a66d68437aeff84e3a3cc8bebee0d2eea"]
)
],
# what data we want returned from the inputs we loaded
field_selection=hyperfuel.FieldSelection(
input=[
InputField.TX_ID,
InputField.BLOCK_HEIGHT,
InputField.INPUT_TYPE,
InputField.UTXO_ID,
InputField.OWNER,
InputField.AMOUNT,
InputField.ASSET_ID,
]
)
)
res = await client.get_selected_data(query)
print("inputs: " + str(res.data.inputs))
asyncio.run(main())
Node Js (repo)
async function main() {
const client = HyperfuelClient.new({
url: "https://fuel-testnet.hypersync.xyz",
});
const query: Query = {
// start query from block 0
fromBlock: 0,
// if to_block is not set, query runs to the end of the chain
toBlock: 1300000,
// load inputs that have `asset_id` = 0x2a0d0ed9d2217ec7f32dcd9a1902ce2a66d68437aeff84e3a3cc8bebee0d2eea
inputs: [
{
assetId: [
"0x2a0d0ed9d2217ec7f32dcd9a1902ce2a66d68437aeff84e3a3cc8bebee0d2eea",
],
},
],
// fields we want returned from loaded inputs
fieldSelection: {
input: [
"tx_id",
"block_height",
"input_type",
"utxo_id",
"owner",
"amount",
"asset_id",
],
},
};
const res = await client.getSelectedData(query);
console.log(`inputs: ${JSON.stringify(res.data.inputs)}`);
}
main();
Json Api (repo)
curl --request POST \
--url https://fuel-testnet.hypersync.xyz/query \
--header 'Content-Type: application/json' \
--data '{
"from_block": 0,
"to_block": 1300000,
"inputs": [
{
"asset_id": ["0x2a0d0ed9d2217ec7f32dcd9a1902ce2a66d68437aeff84e3a3cc8bebee0d2eea"]
}
],
"field_selection": {
"input": [
"tx_id",
"block_height",
"input_type",
"utxo_id",
"owner",
"amount",
"asset_id"
]
}
}'
Query Structure
File: HyperFuel/hyperfuel-query.md
This section is dedicated to giving an exhaustive list of all the fields and query parameters of a HyperFuel query. HyperFuel is extremely powerful but learning how to craft queries can take some practice. It is recommended to look at the examples and reference this page. HyperFuel query structure is the same across clients.
Top-level query structure
Illustrated as json
{
// The block to start the query from
"from_block": Number,
// The block to end the query at. If not specified, the query will go until the
// end of data. Exclusive, the returned range will be [from_block..to_block).
//
// The query will return before it reaches this target block if it hits the time limit
// configured on the server. The user should continue their query by putting the
// next_block field in the response into from_block field of their next query. This implements
// pagination.
"to_block": Number, // Optional, defaults to latest block
// List of receipt selections, the query will return receipts that match any of these selections.
// All selections have an OR relationship with each other.
"receipts": [{ReceiptSelection}], // Optional
// List of input selections, the query will return inputs that match any of these selections.
// All selections have an OR relationship with each other.
"inputs": [{InputSelection}], // Optional
// List of output selections, the query will return outputs that match any of these selections.
// All selections have an OR relationship with each other.
"outputs": [{OutputSelection}], // Optional
// Whether to include all blocks regardless of whether they match a receipt, input, or output selection. Normally
// The server will return only the blocks that are related to the receipts, inputs, or outputs in the response. But if this
// is set to true, the server will return data for all blocks in the requested range [from_block, to_block).
"include_all_blocks": bool, // Optional, defaults to false
// The user selects which fields they want returned. Requesting fewer fields will improve
// query execution time and reduce the payload size so the user should always use a minimal number of fields.
"field_selection": {FieldSelection},
// Maximum number of blocks that should be returned, the server might return more blocks than this number but
//It won't overshoot by too much.
"max_num_blocks": Number, // Optional, defaults to no maximum
}
ReceiptSelection
The query takes an array of ReceiptSelection objects and returns receipts that match any of the selections. All fields are optional. Below is an exhaustive list of all fields in a ReceiptSelection JSON object. Reference the Fuel docs on receipts for field explanations.
{
// address that emitted the receipt
"root_contract_id": [String],
// The recipient address
"to_address": [String],
// The asset id of the coins transferred.
"asset_id": [String],
// the type of receipt
// 0 = Call
// 1 = Return,
// 2 = ReturnData,
// 3 = Panic,
// 4 = Revert,
// 5 = Log,
// 6 = LogData,
// 7 = Transfer,
// 8 = TransferOut,
// 9 = ScriptResult,
// 10 = MessageOut,
// 11 = Mint,
// 12 = Burn,
"receipt_type": [Number],
// The address of the message sender.
"sender": [String],
// The address of the message recipient.
"recipient": [String],
// The contract id of the current context is in an internal context. null otherwise
"contract_id": [String],
// receipt register values.
"ra": [Number],
"rb": [Number],
"rc": [Number],
"rd": [Number],
// the status of the transaction that the receipt originated from
// 1 = Success
// 3 = Failure
"tx_status": [Number],
// the type of the transaction that the receipt originated from
// 0 = script
// 1 = create
// 2 = mint
// 3 = upgrade
// 4 = upload
"tx_type": [Number]
}
InputSelection
The query takes an array of InputSelection objects and returns inputs that match any of the selections. All fields are optional. Below is an exhaustive list of all fields in an InputSelection JSON object. Reference the Fuel docs on inputs for field explanations.
{
// The owning address or predicate root.
"owner": [String],
// The asset ID of the coins.
"asset_id": [String],
// The input contract.
"contract": [String],
// The sender address of the message.
"sender": [String],
// The recipient address of the message.
"recipient": [String],
// The type of input
// 0 = InputCoin,
// 1 = InputContract,
// 2 = InputMessage,
"input_type": [Number],
// the status of the transaction that the input originated from
// 1 = Success
// 3 = Failure
"tx_status": [Number],
// the type of the transaction that the input originated from
// 0 = script
// 1 = create
// 2 = mint
// 3 = upgrade
// 4 = upload
"tx_type": [Number]
}
OutputSelection
The query takes an array of OutputSelection objects and returns outputs that match any of the selections. All fields are optional. Below is an exhaustive list of all fields in an OutputSelection JSON object. Reference the Fuel docs on outputs for field explanations.
{
// The address the coins were sent to.
"to": [String],
// The asset id for the coins sent.
"asset_id": [String],
// The contract that was created.
"contract": [String],
// the type of output
// 0 = CoinOutput,
// 1 = ContractOutput,
// 2 = ChangeOutput,
// 3 = VariableOutput,
// 4 = ContractCreated,
"output_type": [Number],
// the status of the transaction that the input originated from
// 1 = Success
// 3 = Failure
"tx_status": [Number],
// the type of the transaction that the input originated from
// 0 = script
// 1 = create
// 2 = mint
// 3 = upgrade
// 4 = upload
"tx_type": [Number]
}
FieldSelection
The query takes a FieldSelection JSON object where the user specifies what they want returned from data matched by their ReceiptSelection, OutputSelection, and InputSelection. There is no BlockSelection or TransactionSelection because the query returns all blocks and transactions that include the data you specified in your ReceiptSelection, OutputSelection, or InputSelection.
For best performance, select a minimal amount of fields.
Important note: all fields draw inspiration from Fuel's graphql schema. Mainly Blocks, Transactions, Receipts, Inputs, and Outputs. Enums of each type (ex: Receipt has 12 different types, two of which are Log and LogData, Input has 3: InputCoin, InputContract, InputMessage, and Output has 5: CoinOutput, ContractOutput, ChangeOutput, VariableOutput, ContractCreated) are flattened into the parent type. This is why multiple fields on any returned Receipt, Input, or Output might be null; it's not a field on all possible enums of that type, so null is inserted.
All fields are optional. Below is an exhaustive list of all fields in a FieldSelection JSON object.
{
"block": [
"id",
"da_height",
"consensus_parameters_version",
"state_transition_bytecode_version",
"transactions_count",
"message_receipt_count",
"transactions_root",
"message_outbox_root",
"event_inbox_root",
"height",
"prev_root",
"time",
"application_hash"
],
"transaction": [
"block_height",
"id",
"input_asset_ids",
"input_contracts",
"input_contract_utxo_id",
"input_contract_balance_root",
"input_contract_state_root",
"input_contract_tx_pointer_tx_index",
"input_contract",
"policies_tip",
"policies_witness_limit",
"policies_maturity",
"policies_max_fee",
"script_gas_limit",
"maturity",
"mint_amount",
"mint_asset_id",
"mint_gas_price",
"tx_pointer_block_height",
"tx_pointer_tx_index",
"tx_type",
"output_contract_input_index",
"output_contract_balance_root",
"output_contract_state_root",
"witnesses",
"receipts_root",
"status",
"time",
"reason",
"script",
"script_data",
"bytecode_witness_index",
"bytecode_root",
"subsection_index",
"subsections_number",
"proof_set",
"consensus_parameters_upgrade_purpose_witness_index",
"consensus_parameters_upgrade_purpose_checksum",
"state_transition_upgrade_purpose_root",
"salt"
],
"receipt": [
"receipt_index",
"root_contract_id",
"tx_id",
"tx_status",
"tx_type",
"block_height",
"pc",
"is",
"to",
"to_address",
"amount",
"asset_id",
"gas",
"param1",
"param2",
"val",
"ptr",
"digest",
"reason",
"ra",
"rb",
"rc",
"rd",
"len",
"receipt_type",
"result",
"gas_used",
"data",
"sender",
"recipient",
"nonce",
"contract_id",
"sub_id"
],
"input": [
"tx_id",
"tx_status",
"tx_type",
"block_height",
"input_type",
"utxo_id",
"owner",
"amount",
"asset_id",
"tx_pointer_block_height",
"tx_pointer_tx_index",
"witness_index",
"predicate_gas_used",
"predicate",
"predicate_data",
"balance_root",
"state_root",
"contract",
"sender",
"recipient",
"nonce",
"data"
],
"output": [
"tx_id",
"tx_status",
"tx_type",
"block_height",
"output_type",
"to",
"amount",
"asset_id",
"input_index",
"balance_root",
"state_root",
"contract",
]
}
Frequently Asked Questions (FAQ)
What is HyperSync?
HyperSync is Envio's purpose-built, high-performance blockchain data retrieval layer, built in Rust. It serves as a direct alternative to traditional JSON-RPC endpoints, providing dramatically faster queries and more flexible data access patterns. Developers use HyperSync to retrieve logs, transactions, traces, and block data at speeds not possible with standard RPC.
How much faster is HyperSync than traditional RPC?
HyperSync is up to 2000x faster than traditional RPC methods. For example, scanning the Arbitrum blockchain for sparse log data takes 2 seconds with HyperSync versus hours or days with traditional RPC. Fetching all Uniswap V3 PoolCreated events on Ethereum takes seconds versus hours - approximately 500x faster.
What blockchains does HyperSync support?
HyperSync supports 70+ EVM-compatible chains and the Fuel Network, with new networks added regularly. You connect to different networks simply by changing the client URL (e.g. https://eth.hypersync.xyz for Ethereum, https://arbitrum.hypersync.xyz for Arbitrum). See the Supported Networks page for the full list.
What client libraries are available?
HyperSync has official client libraries for Python, Rust, Node.js, and Go.
Do I need an API token?
Yes. An API token is required to use HyperSync. Set it as an environment variable:
export ENVIO_API_TOKEN="your-api-token-here"
How do I get started quickly?
The fastest way to see HyperSync in action with zero setup is LogTUI:
pnpx logtui aave arbitrum
To start building, clone the quickstart repository:
git clone https://github.com/enviodev/hypersync-quickstart.git
cd hypersync-quickstart
pnpm install
node run-simple.js
What types of data can I query with HyperSync?
HyperSync supports querying four types of blockchain data: logs (events), transactions, traces (internal transactions and state changes - supported on select networks like Ethereum Mainnet), and blocks. Each type supports fine-grained filtering and field selection.
What is field selection and why does it matter?
Field selection lets you specify exactly which fields you want returned in a query (e.g. only Address, Topic0, and Data from logs). This dramatically reduces unnecessary data transfer and improves query performance - you only pay for the data you actually need.
What are join modes?
Join modes control how related data is returned alongside your query results. Options include JoinNothing (exact matches only), JoinAll (matches plus all related objects), JoinTransactions (matches plus their transactions), and the default (a reasonable set of related objects).
What is the relationship between HyperSync and HyperIndex?
HyperSync is the data engine - it provides raw, high-speed access to blockchain data. HyperIndex is the full-featured indexing framework built on top of HyperSync, adding schema management, event handling, and GraphQL APIs. Use HyperSync directly when you need raw blockchain data at maximum speed, or use HyperIndex when you need a complete indexing solution.
What is LogTUI?
LogTUI is a terminal-based blockchain event viewer built on HyperSync. It lets you monitor events from popular protocols (Uniswap, Aave, Chainlink, ENS, and 20+ others) across multiple chains with zero configuration. Run it with a single pnpx logtui command.
Where can I get help?
- Discord: discord.gg/envio - the fastest way to get help from the team and community
- Telegram: Envio Telegram - the offcial Envio Telegram to get support from the team and community
- GitHub: github.com/enviodev
- Email: hello@envio.dev