A Rust-based agent system that generates and submits gas price predictions for EVM networks to the Gas Network for evaluation.
You'll need Rust installed on your system. The recommended way is using rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/envDownload and run the installer from rustup.rs
Verify your installation:
rustc --version
cargo --versionDownload the latest release for your platform:
# Install using our shell installer (macOS/Linux)
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/blocknative/gas-agent/releases/latest/download/gas-agent-installer.sh | sh
# Or install via Homebrew (macOS/Linux)
brew install blocknative/tap/gas-agent
# Or download manually from GitHub releases
# Visit: https://github.com/blocknative/gas-agent/releasesIf you prefer to build from source or need the latest unreleased changes:
# Install via cargo (requires Rust toolchain)
cargo install --git https://github.com/blocknative/gas-agent
# Or clone and build
git clone https://github.com/blocknative/gas-agent
cd gas-agent
cargo build --release-
Generate signing keys
All agent payloads are signed before submission to the Gas Network so that they are verifiable and attributable. Use the private key for the
signer_keyfield for each agent.gas-agent generate-keys
-
Configure chains and agents
A list of chains and the agents to run for each chain can be configured and will run in parallel with each chain running on it's own thread. Set the
CHAINSenv variable with a JSON string:[ { "system": "ethereum", "network": "mainnet", "json_rpc_url": "https://ethereum-rpc.publicnode.com", "agents": [ { "kind": "percentile", "signer_key": "YOUR-GENERATED-PRIVATE-KEY", "prediction_trigger": "block" } ] } ] -
Run the agent
gas-agent start
Before your agent can submit predictions to the Gas Network, you need to register and get your signing addresses whitelisted.
- Signed Predictions: All agent payloads are cryptographically signed using your private key before submission
- Address Extraction: The Gas Network collector validates the signature and extracts the corresponding Ethereum address
- Whitelist Validation: Only predictions from whitelisted addresses are accepted and processed by the network
To register your agent and get your signing addresses whitelisted:
-
Generate Your Keys: Use the built-in key generation tool to create your signing keys:
gas-agent generate-keys
-
Save Your Keys: Securely store the private key for your agent configuration and note the corresponding public address
-
Submit Whitelist Request: Contact the Blocknative team with your public address(es):
- Email: [email protected]
- Discord: Join our community at https://discord.com/invite/KZaBVME
-
Include in Your Request:
- Your Ethereum address(es) that will be signing predictions
- Brief description of your prediction model/strategy
- Expected prediction frequency and settlement types
- Your intended use case or research goals
- Unique Keys Recommended: Use different signing keys for different agent types or models
- Separate Evaluation: Each unique combination of address, system, network, and settlement is evaluated independently
- All Addresses Need Whitelisting: Each signing address you plan to use must be individually whitelisted
While waiting for whitelist approval, you can:
- Test your agent locally with mock endpoints
- Verify your prediction logic and model performance
- Ensure your signing and payload generation works correctly
Once whitelisted, your agent can begin submitting predictions that will be evaluated and potentially published to the Gas Network for end users.
Before starting development, ensure you have:
- Rust toolchain (see Installation section above)
- Git for version control
- A code editor with Rust support (VS Code with rust-analyzer recommended)
# Clone the repository
git clone https://github.com/blocknative/gas-agent
cd gas-agent
# Verify everything builds and tests pass
cargo check
cargo test
cargo clippy --workspace --all-targets --all-features
cargo fmt --check# Run with debug logging
RUST_LOG=debug cargo run -- start --chains 'YOUR-CONFIG-JSON'
# Run tests
cargo test
# Run specific tests
cargo test test_name
# Run with release optimizations (for performance testing)
cargo run --release -- start --chains 'YOUR-CONFIG-JSON'-
Create a feature branch:
git checkout -b feature/your-feature-name
-
Make your changes and ensure code quality:
# Check for compilation errors cargo check # Run tests cargo test # Fix linting issues cargo clippy --workspace --all-targets --all-features --fix # Format code cargo fmt
-
Commit your changes:
git add . git commit -m "Add your feature description"
-
Push and create a pull request:
git push -u origin feature/your-feature-name # Then create a PR on GitHub
The project enforces strict code quality standards:
- No warnings: All code must compile without warnings
- Formatted code: Use
cargo fmtto format code consistently - Linted code: Use
cargo clippyto catch common mistakes and improve code quality - Tested code: Add tests for new functionality
- Documentation: Document public APIs and complex logic
# Run all tests
cargo test
# Run tests with output
cargo test -- --nocapture
# Run tests for a specific module
cargo test models::
# Run integration tests only
cargo test --test '*'
# Generate test coverage report (requires cargo-tarpaulin)
cargo install cargo-tarpaulin
cargo tarpaulin --out HtmlThe agent supports the following command-line arguments and environment variables:
--server-address/SERVER_ADDRESS: HTTP server bind address (currently used only for Kubernetes probes) (default:0.0.0.0:8080)--chains/CHAINS: JSON configuration for EVM networks and agents--collector-endpoint/COLLECTOR_ENDPOINT: The Gas Network endpoint for payload evaluation (default:https://collector.gas.network)
The chain configuration is specified as a JSON array where each object represents an EVM network and its associated agents. Each chain configuration supports the following fields:
-
system(required): The blockchain system to connect to- Available options:
"ethereum","base","polygon"
- Available options:
-
network(required): The network within the system- Available options:
"mainnet"
- Available options:
-
json_rpc_url(required): The JSON-RPC endpoint URL to poll for new blocks- Example:
"https://ethereum-rpc.publicnode.com"
- Example:
-
pending_block_data_source(optional): Configuration for fetching pending-block (mempool) data- See Pending Block Data Source section below
-
agents(required): Array of agent configurations to run on this chain- See Agent Configuration section below
When specified, this configures how to fetch pending block (mempool) data which can be passed to models that can be more reactive to changes in the mempool and to make use of private data to create more accurate predictions:
{
"pending_block_data_source": {
"json_rpc": {
"url": "https://api.example.com/pending",
"method": "eth_getPendingBlock",
"params": ["pending"],
"poll_rate_ms": 1000
}
}
}A full CHAINS configuration for Ethereum with a JSON RPC pending block data source:
CHAINS='[{"system": "ethereum", "network": "mainnet", "json_rpc_url": "https://ethereum-rpc.publicnode.com", "pending_block_data_source": {"json_rpc": {"url": "http://localhost:8545", "method": "eth_getBlockByNumber", "params": ["pending", true], "poll_rate_ms": 1000}}, "agents": [{"kind": "pending_floor", "prediction_trigger": {"poll": {"rate_ms": 1000}}, "signer_key": "0xede33830a316e1f3018a50709c54d1f01d6ab3053f4f29ec552ce10bc9f5ef72"}]}]'Fields:
url(required): The JSON-RPC endpoint URLmethod(required): The RPC method to callparams(optional): Parameters to pass to the RPC methodpoll_rate_ms(required): Polling interval in milliseconds
Currently, JSON RPC is the only source available, but other sources are coming soon. Please create an issue if there is a specific source that you would like to see supported.
The pending block RPC endpoint must return a JSON-RPC 2.0 response containing a required transactions field with an array of transaction objects. Each transaction object has the following requirements:
Required fields:
hash: Transaction hash identifier
Gas price fields (one of the following combinations):
gasPrice: Legacy gas price (for pre-EIP-1559 transactions)- OR
maxFeePerGasANDmaxPriorityFeePerGas: EIP-1559 gas price fields (both required together)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"transactions": [
{
"hash": "0x1234567890abcdef...",
"maxFeePerGas": "0x174876e800",
"maxPriorityFeePerGas": "0x59682f00"
},
{
"hash": "0xabcdef1234567890...",
"gasPrice": "0x165a0bc00"
}
]
}
}Each agent in the agents array supports the following configuration:
{
"kind": "percentile",
"signer_key": "0x1234567890abcdef...",
"prediction_trigger": "block"
}Fields:
-
kind(required): The type of agent to run"node": Publishes the standard estimate from the node"target": Publishes the actual minimum price for new blocks- Model-based agents:
"adaptive_threshold": Uses adaptive threshold analysis"distribution_analysis": Analyzes gas price distributions"moving_average": Uses moving average calculations"percentile": Uses percentile-based predictions"time_series": Uses time series analysis"last_min": Takes the minimum from the previous block and uses that as the prediction for the next block."pending_floor": Takes the minimum from the pending-block, adds 1 wei and uses that as the prediction for the next block.
-
signer_key(required): Private key for signing predictions (usecargo run -- generate-keysto create) -
prediction_trigger(required): When to generate predictions"block": Generate prediction when a new block is detected{"poll": {"rate_ms": 5000}}: Generate predictions at regular intervals (rate in milliseconds)
The gas agent includes several built-in prediction models that analyze block data to estimate optimal gas prices. Each model uses different strategies and data sources to predict gas prices. All models now return errors when they lack sufficient data instead of fallback values, providing clear feedback about what's needed for successful predictions.
These models use historical onchain data to create a prediction for the next block. Since these models can only generate a single prediction per block, the "prediction_trigger": "block" is typically used with these models. All historical models require at least one block with transaction data and will return descriptive errors if insufficient data is provided.
Analyzes the distribution of gas prices across the 5 most recent blocks and selects the 75th percentile to ensure high inclusion probability. This model is particularly effective during periods of high volatility, as it targets a price that would have included 75% of recent transactions. Requires at least one block distribution with transactions.
Simply takes the minimum gas price from the most recent block and uses it as the prediction for the next block. This is the most aggressive pricing strategy and works well when gas prices are stable. Requires at least one non-empty block distribution.
Calculates a Simple Weighted Moving Average (SWMA) of recent gas prices, giving more weight to more recent blocks (up to 10 blocks). This approach works well when gas prices are relatively stable and provides smooth price transitions. Requires at least one block with transactions.
Identifies the minimum gas price that would have been included in each recent block (up to 50 blocks) and applies an adaptive premium based on price volatility. When prices are stable, it applies a small premium; when volatile, it applies a larger premium (up to 50%). This provides a balance between cost and inclusion probability. Requires at least one block distribution with transactions.
Uses simple linear regression to identify trends in gas prices and predict the next value based on the median gas price of the last 20 blocks. This model is particularly useful when gas prices show a consistent trend over time (either increasing or decreasing). Requires at least one block with transactions for analysis.
Analyzes the cumulative distribution function (CDF) of gas prices in the most recent block to find "sweet spots" where many transactions are being included. It identifies points where the rate of change in the CDF decreases significantly, representing efficient gas price levels, then applies a 10% premium for higher inclusion probability. Requires at least one block distribution with a non-empty latest block.
These models use pending (mempool or private) transaction data to make predictions. They are specifically designed for users who have access to pending block information, such as block builders with proprietary transaction flows.
Specifically designed for block builders with proprietary private transaction flow who can see what the likely next block will contain. This model analyzes the pending block distribution to find the minimum gas price and adds exactly 1 wei (0.000000001 gwei) to ensure transaction inclusion while paying the absolute minimum. Unlike historical models, this one requires access to pending block data and will return an error if no pending block distribution is provided. Most effective when used with a polling prediction trigger to provide up to date predictions ("prediction_trigger": {"poll": {"rate_ms: <desired_rate>"}}).
All models will return descriptive errors instead of fallback values when they lack sufficient data. This provides clear feedback about what's needed for successful predictions.
Common Error Scenarios:
- Empty block distributions: All historical models require at least one block distribution
- No transactions: Models need blocks that contain actual transaction data to analyze
- Missing pending data: The
pending_floormodel specifically requires pending block distribution data
Error Message Examples:
"LastMin model requires at least one block distribution""Percentile model requires blocks with transactions""PendingFloor model requires pending block distribution data"
When a model returns an error, check that:
- You're providing the correct type of data for the model
- Your block distributions contain actual transaction data
- For
pending_floor, you're providing thepending_block_distributionparameter
To create a custom prediction model, you'll need to fork the repository and implement your own model logic. Here's how:
git fork https://github.com/blocknative/gas-agent
git clone https://github.com/YOUR-USERNAME/gas-agent
cd gas-agentAdd your model to the ModelKind enum in src/types.rs:
#[derive(Debug, Clone, EnumString, Display, Deserialize, Serialize)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ModelKind {
AdaptiveThreshold,
DistributionAnalysis,
MovingAverage,
Percentile,
TimeSeries,
LastMin,
PendingFloor,
YourCustomModel, // Add this line
}Create a new file src/models/your_custom_model.rs:
/*
Your Custom Model Description
Explain what your model does and how it works.
*/
use crate::models::{FromBlock, ModelError, Prediction};
use crate::types::Settlement;
use crate::{distribution::BlockDistribution, utils::round_to_9_places};
pub fn get_prediction_your_custom_model(
block_distributions: &[BlockDistribution],
latest_block: u64,
) -> Result<(Prediction, Settlement, FromBlock), ModelError> {
// Your model logic here
//
// block_distributions is a Vec<BlockDistribution> where:
// - BlockDistribution = Vec<Bucket>
// - Bucket { gwei: f64, count: u32 }
//
// Each BlockDistribution represents gas price buckets from a block
// sorted from oldest to newest blocks
let Some(latest_block_distribution) = block_distributions.last() else {
return Err(ModelError::insufficient_data(
"YourCustomModel requires at least one block distribution",
));
};
let mut total_gas_price = 0.0;
let mut total_transactions = 0u32;
for bucket in latest_block_distribution {
total_gas_price += bucket.gwei * bucket.count as f64;
total_transactions += bucket.count;
}
if total_transactions == 0 {
return Err(ModelError::insufficient_data(
"YourCustomModel requires blocks with transactions",
));
}
let predicted_price = total_gas_price / total_transactions as f64;
Ok((
round_to_9_places(predicted_price),
Settlement::Fast,
latest_block + 1,
))
}Add your model to the module system in src/models/mod.rs:
use your_custom_model::get_prediction_your_custom_model;
mod your_custom_model;
// In the apply_model function, add your case:
pub async fn apply_model(
model: &ModelKind,
block_distributions: &[BlockDistribution],
pending_block_distribution: Option<BlockDistribution>,
latest_block: u64,
) -> Result<(Prediction, Settlement, FromBlock), ModelError> {
// ... existing code ...
match model {
// ... existing cases ...
ModelKind::YourCustomModel => {
get_prediction_your_custom_model(block_distributions, latest_block)
}
}
}cargo build
cargo test
# Test your model
gas-agent start --chains '[{
"system": "ethereum",
"network": "mainnet",
"json_rpc_url": "https://ethereum-rpc.publicnode.com",
"agents": [{
"kind": "your_custom_model",
"signer_key": "YOUR-PRIVATE-KEY",
"prediction_trigger": "block"
}]
}]'-
Understand the Data Structure: Each
BlockDistributioncontains buckets of gas prices with transaction counts, representing the gas price distribution for that block. -
Handle Edge Cases: Validate inputs and return descriptive
ModelErrors when data is insufficient. -
Consider Settlement Times: Choose appropriate
Settlementvalues:Immediate: Next blockFast: ~15 secondsMedium: ~15 minutesSlow: ~1 hour
-
Use Utility Functions: The
round_to_9_places()function ensures consistent precision across predictions. -
Test with Different Market Conditions: Test your model during periods of high volatility, network congestion, and normal conditions.
-
Leverage Pending Block Data: If available, you can access
pending_block_distributionparameter in theapply_modelfunction for more reactive predictions.
As a prediction provider, you can generate gas price predictions for different settlement times that end users will consume to price their transactions. Each settlement time represents a different urgency level and block window that your predictions target. A settlement BlockWindow is derived from the from_block parameter and Settlement enum. The from_block must be a future block number and is typically set to the next block.
Your models can return one of four settlement times, each targeting different end-user needs:
- Target Time: Next block (0ms)
- Block Window: 0 blocks (next block only)
- End User Profile: Arbitrage bots, MEV strategies, time-critical DeFi operations
- Prediction Strategy: Should predict the minimum gas price needed for immediate inclusion
- Target Time: ~15 seconds (15,000ms)
- End User Profile: Standard DeFi interactions, swaps, NFT minting
- Prediction Strategy: Balance between inclusion probability and cost efficiency
- Target Time: ~15 minutes (900,000ms)
- End User Profile: Regular transfers, non-urgent contract interactions
- Prediction Strategy: Focus on cost optimization while maintaining reasonable inclusion probability
- Target Time: ~1 hour (3,600,000ms)
- End User Profile: Batch operations, low-priority transactions, cost-sensitive users
- Prediction Strategy: Minimize gas costs, accepting longer wait times
Settlement times are converted to block windows based on each network's block time:
- Ethereum: 12 seconds per block (12,000ms)
- Base: 2 seconds per block (2,000ms)
- Polygon: 2 seconds per block (2,000ms)
The number of blocks for each settlement is calculated as: floor(settlement_time_ms / network_block_time_ms)
| Settlement | Ethereum (12s blocks) | Base (2s blocks) | Polygon (2s blocks) |
|---|---|---|---|
immediate |
0 blocks (next block) | 0 blocks (next block) | 0 blocks (next block) |
fast |
~1 block | ~8 blocks | ~8 blocks |
medium |
~75 blocks | ~450 blocks | ~450 blocks |
slow |
~300 blocks | ~1,800 blocks | ~1,800 blocks |
When you submit a gas price prediction, the Gas Network evaluates its accuracy within the specified block window:
- Prediction Submitted: Your model predicts 20 gwei with
fastsettlement for Ethereum - Block Window Calculated:
faston Ethereum = ~1 block window starting fromfrom_block - Evaluation Period: The system monitors min price for blocks within that window
- Scoring: Your prediction is scored on:
- Inclusion Rate: Did your prediction price get onchain within the block window
- Cost Efficiency: Percentage overpayment
For details on the Evaluation Function used to score your predictions, see the Evaluation Function.
# Optimized release build
cargo build --release
# The binary will be available at
./target/release/gas-agentReleases are automated using cargo-dist and triggered by pushing git tags. Follow these steps:
Update the version in Cargo.toml:
[package]
version = "0.1.0" # Update thisUpdate CHANGELOG.md following Keep a Changelog format:
## [0.1.0] - 2025-01-22
### Added
- New feature descriptions
- New model implementations
### Changed
- Breaking changes or significant modifications
- Performance improvements
### Fixed
- Bug fixes and error handling improvements
### Removed
- Deprecated features that were removedImportant: The GitHub release title and body are automatically generated from the changelog, so ensure your changelog entries are clear and well-formatted.
# Commit version and changelog changes first
git add Cargo.toml CHANGELOG.md
git commit -m "Prepare release v0.1.0"
# Create and push the tag
git tag v0.1.0
git push origin v0.1.0When you push the tag, GitHub Actions will automatically:
- Build artifacts for multiple platforms (Linux, macOS, Windows)
- Run tests to ensure code quality
- Generate release notes from your changelog
- Create GitHub release with:
- Auto-generated title from changelog
- Auto-generated body from changelog
- Platform-specific binaries and installers
- Checksums for verification
After the workflow completes:
- Check the GitHub releases page
- Verify all platform binaries are present
- Test the installer scripts work correctly
- Update documentation if needed
Before creating a release:
- All tests pass:
cargo test - Code is properly formatted:
cargo fmt --check - No clippy warnings:
cargo clippy --workspace --all-targets --all-features - Version updated in
Cargo.toml -
CHANGELOG.mdupdated with new version and changes - All changes committed to main branch
- Git tag created and pushed
For urgent fixes:
- Create a hotfix branch from the release tag
- Apply minimal fix
- Update patch version (e.g.,
0.1.0→0.1.1) - Update changelog with hotfix entry
- Create new tag and push
For beta or release candidate versions:
- Use pre-release version format:
v0.1.0-beta.1 - GitHub release will automatically be marked as pre-release
- Follow same process as regular releases
A Dockerfile is included for containerized deployments:
docker build -t gas-agent .
docker run -e CHAINS='YOUR-CONFIG' gas-agent startThe agent exposes kubernetes probe endpoints:
- Liveness:
GET /internal/probe/liveness - Readiness:
GET /internal/probe/readiness
See LICENSE file for details.