Skip to main content

How to Build Custom RPC Methods with Reth

Updated on
Sep 26, 2025

18 min read

Overview

Ever wanted to build your own custom Web3 API? Reth now allows you to extend the standard Ethereum JSON-RPC API with custom functionality without the need for forking the codebase. This opens up possibilities like building a Marketplace add-on to be used for DeFi, specialized indexers, and more.

This guide will walk you through building and deploying a custom RPC method with Reth. Specifically, we'll create an RPC method called eth_getBlockUtilization that retrieves the current or historical block utilization rate.

To learn more about how you can build your own Marketplace Add-on, check out this guide

What You Will Learn


  • Learn about Reth
  • Build a custom RPC method
  • Run your Reth node with the custom RPC method configuration
  • Test and validate the custom RPC method

What You Will Need


  • A running Reth node - Follow our How to Run a Reth Node guide first
  • Rust development environment (rustc 1.86+)
  • Linux, macOS, or Windows with WSL
  • Git installed
  • Basic familiarity with Rust programming and JSON-RPC concepts
  • Understanding of Ethereum architecture, transactions and smart contracts

What is Reth?

Reth is a high-performance Ethereum execution client written in Rust. It's designed to be modular, fast, and developer friendly while maintaining full compatibility with the Ethereum.

Custom RPC Methods on Reth

Reth now allows natively building custom RPC methods without needing to maintain chain updates, like how you might in a forked codebase. This means less time debugging and maintaining code. To utilize a custom RPC method, you'll need to enable the --extended-eth-namespace flag when running your Reth node. This means we'll have to stop our node briefly in order to restart it with the configuration needed. Let's dive into the code.

Code Review

This guide covers building a method called eth_getBlockUtilization, which can query the current or historical block utilization rate.

The logic for this custom method will live in the reth/src/main.rs file. Let's go over the code piece by piece.

Imports

//! Custom Reth client with extended ETH namespace for block utilization metrics
#![allow(unused_crate_dependencies)]

use clap::Parser;
use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned};
use reth_ethereum::{
cli::{chainspec::EthereumChainSpecParser, interface::Cli},
node::EthereumNode,
};
use alloy_rpc_types::BlockNumberOrTag;
use reth_provider::{BlockReaderIdExt, HeaderProvider};
use reth_primitives::Header;

Main - fn main()

fn main() {
Cli::<EthereumChainSpecParser, RethCliExtended>::parse()
.run(|builder, args| async move {
let handle = builder
.node(EthereumNode::default())
.extend_rpc_modules(move |ctx| {
if !args.extended_eth_namespace {
return Ok(());
}

let provider = ctx.provider().clone();
let ext = BlockUtilizationImpl::new(provider);
ctx.modules.merge_configured(ext.into_rpc())?;

println!("Extended ETH namespace enabled with eth_getBlockUtilization");

Ok(())
})
.launch()
.await?;

handle.wait_for_node_exit().await
})
.unwrap();
}

Whats happening:


  • Cli::<EthereumChainSpecParser, RethCliExtended>::parse() - Uses types to allow different chain specifications and CLI extensions
  • builder.node(EthereumNode::default()) - Sets the base Ethereum node implementation
  • extend_rpc_modules(move) |ctx| - Provides a hook to add custom RPC methods
  • ctx.provider().clone() - Access to the blockchain data provider
  • ctx.modules.merge_configured(ext.into_rpc())? - Registers the new RPC method

Configuration Extension

  #[derive(Default, clap::Args)]
struct RethCliExtended {
#[arg(long)]
extended_eth_namespace: bool,
}

What's happening:


  • #[derive(Default, clap::Args)] - Automatic CLI argument parsing
  • #[arg(long)] - Creates --enable-custom-rpc command line flag
  • The struct extends Reth's base CLI without modifying core code

RPC Trait Definition

#[cfg_attr(not(test), rpc(server, namespace = "eth"))]
#[cfg_attr(test, rpc(server, client, namespace = "eth"))]
/// RPC trait for block utilization methods
pub trait BlockUtilizationNamespace {
/// Returns the gas utilization percentage for a given block
#[method(name = "getBlockUtilization")]
fn get_block_utilization(&self, block_number: BlockNumberOrTag) -> RpcResult<f64>;
}

What's happening:


  • #[rpc(server, namespace = "eth")] - Macro generates JSON-RPC server boilerplate
  • namespace = "eth" - Creates method eth_getBlockUtilization (namespace + method name)
  • BlockNumberOrTag - Standard Ethereum type for specifying blocks (latest, earliest, number)
  • RpcResult<f64> - Standardized error handling for RPC responses returning utilization percentage

Implementation Structure

/// Implementation of block utilization RPC methods
#[derive(Debug)]
pub struct BlockUtilizationImpl<Provider> {
provider: Provider,
}

impl<Provider> BlockUtilizationImpl<Provider>
where
Provider: BlockReaderIdExt + HeaderProvider + Clone + Unpin + 'static,
{
/// Creates a new BlockUtilizationImpl instance
pub fn new(provider: Provider) -> Self {
Self { provider }
}
}

What's happening:


  • Generic Provider type allows flexibility in data source implementation
  • Trait bounds ensure provider supports block reading and header access
  • Clone + Unpin + static requirements for async RPC handling

Core Logic Implementation

impl<Provider> BlockUtilizationNamespaceServer for BlockUtilizationImpl<Provider>
where
Provider: BlockReaderIdExt + HeaderProvider + Clone + Unpin + 'static,
Provider::Header: Into<Header> + Clone,
{
fn get_block_utilization(&self, block_number: BlockNumberOrTag) -> RpcResult<f64> {
// Convert BlockNumberOrTag to actual block number
let block_num = match block_number {
BlockNumberOrTag::Number(num) => num,
BlockNumberOrTag::Latest => {
match self.provider.best_block_number() {
Ok(num) => num,
Err(e) => return Err(ErrorObjectOwned::owned(
-32000,
format!("Failed to get latest block number: {}", e),
None::<()>,
)),
}
}
BlockNumberOrTag::Earliest => 0,
BlockNumberOrTag::Pending => {
return Err(ErrorObjectOwned::owned(
-32000,
"Pending block not supported for utilization calculation",
None::<()>,
));
}
_ => return Err(ErrorObjectOwned::owned(
-32000,
"Unsupported block number tag",
None::<()>,
)),
};

// Get the header using HeaderProvider
let header = match self.provider.header_by_number(block_num) {
Ok(Some(header)) => header,
Ok(None) => return Err(ErrorObjectOwned::owned(
-32000,
"Block header not found",
None::<()>,
)),
Err(e) => return Err(ErrorObjectOwned::owned(
-32000,
format!("Provider error: {}", e),
None::<()>,
)),
};

// Convert to Header type and access fields directly
let header: Header = header.into();
let gas_limit = header.gas_limit;
let gas_used = header.gas_used;

if gas_limit == 0 {
return Ok(0.0);
}

let utilization = (gas_used as f64 / gas_limit as f64) * 100.0;
let utilization = (utilization * 100.0).round() / 100.0;

Ok(utilization)
}
}

What's happening:


  • Block number resolution handles different tag types (latest, earliest, specific block number)
  • Header retrieval with comprehensive error handling for missing blocks
  • Utilization calculation: (gas_used / gas_limit) * 100
  • Result rounded to 2 decimal places for consistent formatting
  • Returns 0.0 for blocks with zero gas limit (edge case handling)

Now that we have a better understanding of the code, let's get into the project set up.

Prerequisites Check

First, for a sanity check, we'll verify our environment, check that our node is running and that it is synced. Note that you may need to update the cURL request to point to a different PORT if yours is not running on localhost:8545.

# Check Rust version (need 1.86+)
rustc --version

# Check if you have a running Reth node
systemctl status reth.service

# Verify your node is synced
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \
localhost:8545

Verify the node is synced before continuing to ensure we can fetch the latest block.

Setting Up the Reth Project

We'll modify an existing Reth installation to add our custom RPC methods. Note that you may need to update your PATH where needed.


  1. First, navigate to your Reth directory:
cd /data/reth 

Important: Adjust path to your Reth installation

  1. Then, backup your current main.rs:
cp bin/reth/src/main.rs bin/reth/src/main.rs.backup

Remember to save the file.

Implementing Block Utilization RPC Methods

Now let's implement our custom RPC methods that provide our block utilization metric.

Replace the contents of bin/reth/src/main.rs with the code below that we went over earlier in the guide:

use clap::Parser;
use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned};
use reth_ethereum::{
cli::{chainspec::EthereumChainSpecParser, interface::Cli},
node::EthereumNode,
};
use alloy_rpc_types::BlockNumberOrTag;
use reth_provider::{BlockReaderIdExt, HeaderProvider};
use reth_primitives::Header;

fn main() {
Cli::<EthereumChainSpecParser, RethCliExtended>::parse()
.run(|builder, args| async move {
let handle = builder
.node(EthereumNode::default())
.extend_rpc_modules(move |ctx| {
if !args.extended_eth_namespace {
return Ok(());
}

let provider = ctx.provider().clone();
let ext = BlockUtilizationImpl::new(provider);
ctx.modules.merge_configured(ext.into_rpc())?;

println!("Extended ETH namespace enabled with eth_getBlockUtilization");

Ok(())
})
.launch()
.await?;

handle.wait_for_node_exit().await
})
.unwrap();
}

/// Custom CLI args extension that adds the extended namespace flag
#[derive(Debug, Clone, Copy, Default, clap::Args)]
struct RethCliExtended {
/// CLI flag to enable the extended ETH namespace
#[arg(long)]
pub extended_eth_namespace: bool,
}

/// RPC trait defining our block utilization methods
#[cfg_attr(not(test), rpc(server, namespace = "eth"))]
#[cfg_attr(test, rpc(server, client, namespace = "eth"))]
pub trait BlockUtilizationNamespace {
/// Get block utilization as percentage (gas_used / gas_limit * 100)
#[method(name = "getBlockUtilization")]
fn get_block_utilization(&self, block_number: BlockNumberOrTag) -> RpcResult<f64>;
}

/// Implementation struct that holds the blockchain data provider
pub struct BlockUtilizationImpl<Provider> {
provider: Provider,
}

impl<Provider> BlockUtilizationImpl<Provider>
where
Provider: BlockReaderIdExt + HeaderProvider + Clone + Unpin + 'static,
Provider::Header: Into<Header> + Clone,
{
pub fn new(provider: Provider) -> Self {
Self { provider }
}
}

/// Implementation of our custom RPC methods
impl<Provider> BlockUtilizationNamespaceServer for BlockUtilizationImpl<Provider>
where
Provider: BlockReaderIdExt + HeaderProvider + Clone + Unpin + 'static,
Provider::Header: Into<Header> + Clone,
{
fn get_block_utilization(&self, block_number: BlockNumberOrTag) -> RpcResult<f64> {
// Convert BlockNumberOrTag to actual block number
let block_num = match block_number {
BlockNumberOrTag::Number(num) => num,
BlockNumberOrTag::Latest => {
match self.provider.best_block_number() {
Ok(num) => num,
Err(e) => return Err(ErrorObjectOwned::owned(
-32000,
format!("Failed to get latest block number: {}", e),
None::<()>,
)),
}
}
BlockNumberOrTag::Earliest => 0,
BlockNumberOrTag::Pending => {
return Err(ErrorObjectOwned::owned(
-32000,
"Pending block not supported for utilization calculation",
None::<()>,
));
}
_ => return Err(ErrorObjectOwned::owned(
-32000,
"Unsupported block number tag",
None::<()>,
)),
};

// Get the block header using HeaderProvider
let header = match self.provider.header_by_number(block_num) {
Ok(Some(header)) => header,
Ok(None) => return Err(ErrorObjectOwned::owned(
-32000,
"Block header not found",
None::<()>,
)),
Err(e) => return Err(ErrorObjectOwned::owned(
-32000,
format!("Provider error: {}", e),
None::<()>,
)),
};

// Convert to Header type and access gas fields
let header: Header = header.into();
let gas_limit = header.gas_limit;
let gas_used = header.gas_used;

if gas_limit == 0 {
return Ok(0.0);
}

let utilization = (gas_used as f64 / gas_limit as f64) * 100.0;
let utilization = (utilization * 100.0).round() / 100.0;

Ok(utilization)
}
}

Remember to save the file. Now let's move on to compiling the code.

Building the Custom RPC Methods


  1. Compile your custom RPC methods:
source ~/.cargo/env
cd /data/reth
cargo check --bin reth

If you encounter compilation errors, verify all imports are correct and dependencies are properly configured in Cargo.toml.


  1. Build in release mode:
cargo build --release --bin reth

Note: The initial build may take 10-15 minutes as it compiles the entire Reth codebase with your custom RPC method.

Running the Custom RPC Method

Your Reth node requires the --extended-eth-namespace command-line flag to be enabled in order to use the custom RPC method. This allows you to run the same binary with or without the extended functionality.

To do this, start Reth with extended eth-namespace flag:

cd /path/to/reth
./target/release/reth node \
--chain <your-chain-name> \
--datadir /path/to/data/<your-chain-name> \
--http --http.addr 0.0.0.0 --http.port 8545 \
--authrpc.addr 127.0.0.1 --authrpc.port 8551 \
--authrpc.jwtsecret /path/to/jwtsecret \
--full \
--extended-eth-namespace

Remember to update the /path/to/ to your correct path.

Check to make sure your node hasn't ran into any initialization errors and continues to synced with the chain tip.

Systemd Service Configuration

Usually you run nodes via Systemd so you don't need to always keep it running in terminal. To do this, let's first stop our node and then update the ExecStart command to include the extended namespace.

sudo systemctl stop reth.service
sudo nano /etc/systemd/system/reth.service

The full code for our Reth service can be found here.

Update the ExecStart line to include the extended namespace flag:

ExecStart=/path/to/reth/target/release/reth node \
--chain <your-chain-name> \
--datadir /path/to/data/<your-chain-name> \
--http --http.addr 0.0.0.0 --http.port 8545 \
--authrpc.addr 127.0.0.1 --authrpc.port 8551 \
--authrpc.jwtsecret /path/to/jwtsecret \
--full \
--extended-eth-namespace

Where:


  • /path/to/reth: folder where the Reth binary is built
  • <your-chain-name>:your chain (e.g., hoodi)
  • /path/to/data/...: directory for node data
  • /path/to/jwtsecret: path to your JWT secret file

Remember to save the file.

Then, restart the service:

sudo systemctl daemon-reload
sudo systemctl start reth.service
sudo journalctl -u reth.service -f

Now that our method is added, let's test it!

Testing Your Custom RPC Methods

Let's verify our custom RPC method is working correctly with current and historical block data.


  1. Test with latest block:
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockUtilization","params":["latest"],"id":1}' \
localhost:8545

Response:

{"jsonrpc":"2.0","id":1,"result":66.45}

At the time we tested this was block 1247242.

  1. Test with specific block (e.g., 1247240)
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockUtilization","params":["0x130808"],"id":2}' \
localhost:8545

Response:

{"jsonrpc":"2.0","id":1,"result":3.34}

  1. Now, let's test error handling:
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockUtilization","params":["0x99999999"],"id":3}' \
localhost:8545

Response:

{"jsonrpc":"2.0","id":3,"error":{"code":-32000,"message":"Block header not found"}}

To give us another sense of security, let's do a sanity check by cross-checking our results with a block explorer and manually via calling eth_getBlockByNumber.

Let's test block 117500. Etherscan shows 98.61%:

Block Explorer

Now, let's call:

curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x1cafc",false],"id":1}' \
http://127.0.0.1:8545

With the response, we can manually do a calculation by extracting the hex values from gasUsed and gasLimit, converting them to integers and dividing them.

..."gasLimit":"0x2243e7d","gasUsed":"0x21c9f10"...

For example:

Conversion

Calculation

This confirms that our custom eth_getBlockUtilization RPC method is working correctly and providing accurate gas utilization percentages.

Earn Revenue via the QuickNode Marketplace

If you see an opportunity to build a useful RPC method, you could put it on the QuickNode Marketplace and earn revenue. Check out our technical guides and courses to build your own Marketplace add-on.

The custom RPC method you want to build may already be created! This means less time developing and maintaining infrastructure for you. Check out the QuickNode Marketplace to see a list of available add-ons across 70+ chains.

QuickNode Marketplace

Final Thoughts

Congrats! You've successfully learned how to build, deploy, and manage custom RPC methods in Reth. This powerful pattern enables you to extend Ethereum's JSON-RPC API with specialized functionality without needing to maintain a forked codebase. Continue building on the knowledge you've learned by trying the next steps:


  • Explore ideas that can be useful DeFi and NFT ecosystems such as building a RPC method that can swap multiple tokens or NFTs at once, check liquidity across multiple DEXs, or existing methods you currently use but could add a bit of improvement
  • Join the Reth Discord community for advanced discussion
  • Contribute your custom RPC methods back to the Reth ecosystem

Subscribe to our QuickNode Newsletter for more articles and guides on Ethereum infrastructure. Connect with us on Twitter or chat in our Discord community!

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide