Gas Tracker Development
Gas tracker is not just "show eth_gasPrice". A useful tool shows historical dynamics, predicts optimal transaction time, splits EIP-1559 components, and estimates by transaction types. Let's see what's inside a proper gas tracker.
EIP-1559: How gas pricing works on Ethereum
After EIP-1559 (London hard fork, August 2021) gas price consists of two parts:
baseFeePerGas — base fee, burned. Determined by protocol: if block > 50% full, base fee rises by 12.5%; if < 50% — falls. Predictable 1-2 blocks ahead.
maxPriorityFeePerGas (tip) — miner/validator tips. Market part — you compete for block inclusion.
Real transaction cost: min(maxFeePerGas, baseFeePerGas + priorityFee) * gasUsed.
For gas tracker this matters: track both components separately, not just total price.
Architecture: Data collection and storage
Data sources
eth_feeHistory — main method for historical data. Returns baseFee, gasUsedRatio and percentile-statistics of priorityFee for specified block range:
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) })
// Get data for last 100 blocks
// percentiles: [25, 50, 75] — slow/standard/fast
const feeHistory = await client.getFeeHistory({
blockCount: 100,
rewardPercentiles: [25, 50, 75, 95],
})
// feeHistory.baseFeePerGas: BigInt[] (1 more — includes next block)
// feeHistory.reward: BigInt[][] (matrix: block x percentile)
// feeHistory.gasUsedRatio: number[] (0..1)
eth_gasPrice — current price at request moment. Legacy method, for EIP-1559 networks returns baseFee + minimum tip.
Mempool data — pending transactions. eth_getBlockByNumber("pending") or pending tx subscription via WebSocket. Needed for real-time competition analysis.
Storage schema
TimescaleDB (PostgreSQL extension) — optimal for time-series gas data:
CREATE TABLE gas_stats (
time TIMESTAMPTZ NOT NULL,
block_number BIGINT NOT NULL,
base_fee_gwei NUMERIC(20, 9) NOT NULL,
tip_slow NUMERIC(20, 9), -- 25th percentile
tip_standard NUMERIC(20, 9), -- 50th percentile
tip_fast NUMERIC(20, 9), -- 75th percentile
tip_instant NUMERIC(20, 9), -- 95th percentile
gas_used_ratio NUMERIC(5, 4), -- 0..1, block fullness
network VARCHAR(50) NOT NULL DEFAULT 'ethereum'
);
SELECT create_hypertable('gas_stats', 'time');
-- Hourly aggregates for historical charts
SELECT add_continuous_aggregate_policy('gas_stats_hourly', ...);
Retention policy: detailed per-block data — 7 days, hourly aggregates — 1 year, daily aggregates — forever.
Data Collector
Collector runs in background and writes data for each new block:
async function collectGasData() {
const client = createWsClient(WS_RPC_URL)
// Subscribe to new blocks via WebSocket
client.watchBlocks({
onBlock: async (block) => {
const baseFee = block.baseFeePerGas
? Number(block.baseFeePerGas) / 1e9
: null
// Get percentile stats for this block
const history = await client.getFeeHistory({
blockCount: 1,
blockNumber: block.number,
rewardPercentiles: [25, 50, 75, 95],
})
const [slow, standard, fast, instant] = history.reward[0].map(
(r) => Number(r) / 1e9
)
await db.query(
`INSERT INTO gas_stats (time, block_number, base_fee_gwei, tip_slow, tip_standard, tip_fast, tip_instant, gas_used_ratio)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[new Date(Number(block.timestamp) * 1000), block.number, baseFee, slow, standard, fast, instant, history.gasUsedRatio[0]]
)
},
onError: (err) => handleCollectorError(err),
})
}
Gas Prediction Algorithm
Simple output of current baseFee is insufficient. Need estimate: "what to set for next block inclusion with X% probability?"
Next baseFee is predictable exactly:
function predictNextBaseFee(currentBaseFee: bigint, gasUsedRatio: number): bigint {
// EIP-1559: baseFee changes proportional to 50% fullness deviation
const targetRatio = 0.5
const maxChangeDenominator = 8n // 12.5% max change
const delta = gasUsedRatio - targetRatio
const change = (currentBaseFee * BigInt(Math.round(delta * 1000))) / (maxChangeDenominator * 1000n)
return currentBaseFee + change
}
Priority fee — market-driven, harder to predict. Use EMA (exponential moving average) of last N blocks for each percentile:
def calculate_priority_fee_estimate(
recent_tips: list[float], # last 20 blocks, 50th percentile
alpha: float = 0.3, # weight of new data
) -> float:
ema = recent_tips[0]
for tip in recent_tips[1:]:
ema = alpha * tip + (1 - alpha) * ema
return ema
Time-of-day patterns. Gas significantly cheaper at UTC 02:00-08:00 (Europe night and Asia morning before market open). For non-urgent transactions — show "optimal window":
-- Average gas per UTC hour for last 30 days
SELECT
EXTRACT(HOUR FROM time) AS hour_utc,
AVG(base_fee_gwei + tip_standard) AS avg_total_fee
FROM gas_stats
WHERE time > NOW() - INTERVAL '30 days'
GROUP BY hour_utc
ORDER BY avg_total_fee ASC;
API and Frontend
REST endpoints for integration:
GET /api/gas/current — current recommendations {slow, standard, fast, instant}
GET /api/gas/history?period=24h — history with aggregation
GET /api/gas/predict?blocks=5 — forecast for N blocks
GET /api/gas/networks — data for multiple networks
For real-time — WebSocket subscription: client subscribes and receives update on each new block without polling.
Multi-chain support — separate collector per network. L2s (Arbitrum, Optimism, Base) have two-component gas: L2 execution fee + L1 data fee. For Optimism L1 data fee calculated via precompile GasPriceOracle (0x420000000000000000000000000000000000000F).
Technology Stack
| Component | Choice |
|---|---|
| Collector | TypeScript + viem |
| Database | TimescaleDB (PostgreSQL) |
| API | Fastify or Express |
| Cache | Redis (current recommendations) |
| Frontend | React + Recharts/Highcharts |
| Deploy | Docker Compose |
Development of basic version (Ethereum mainnet, history + current recommendations) — 5-8 days. Multi-chain with predictions and detailed analytics — 2-3 weeks.







