In this step-by-step tutorial, we are going to build a blockchain analytics and monitoring dashboard for BNB Chain (formerly Binance Smart Chain) using GetBlock's JSON-RPC API. The project will have a backend server to retrieve on-chain data through polling and a frontend dashboard (React) to visualize the data. We'll cover obtaining a GetBlock API key, setting up recurrent data polling (wallet balances, contract events, gas prices, transaction volumes), building a whale-tracking module for large transactions, organizing the project structure, and showing example JSON-RPC requests for BNB Chain.
Ensure you have a solid understanding of Node.js or Python (backend) and React (frontend) prior to starting. You should also feel at home with JavaScript/TypeScript or Python syntax and have Node.js and npm (or Python and pip) installed, along with Git for project management.
Why GetBlock?
GetBlock is a blockchain node provider that enables you to access blockchain data via JSON-RPC without having to operate your own node. This dramatically simplifies development: "With GetBlock, developers can communicate with the BSC ecosystem seamlessly, significantly reducing the hassle of working with a blockchain network." That is, GetBlock provides reliable BNB Chain RPC endpoints so you can focus on building your app rather than node maintenance.
We will create a monitoring tool that fetches on-chain data from BNB Chain periodically and displays insights on a dashboard. The key features are:
- Periodic polling of wallet balances (to track addresses of interest)
- Listening to contract events/logs (e.g., token transfers or your custom smart contract events)
- Getting on-chain data like the current gas price and transaction volume
- A whale-tracking module to alert on large fund movements in/out of big wallets
- A frontend dashboard (React + Charting library) to present the above data in tables and charts
Let's start by gaining access to BNB Chain RPC using GetBlock.
1. Obtaining a GetBlock API Key
To use GetBlock's BNB Chain node, you need to create an account and get an API key (access token). Follow these steps:
- Sign up on GetBlock. Go to GetBlock and sign up for a free account using your email or Google account. Verify your email and log in.
- Find your API key. Once you've logged in to the GetBlock dashboard, your API key will be shown in the dashboard. Copy the API key - we'll need it to authenticate our requests.
- Select a BNB Smart Chain (BSC) node. In the GetBlock dashboard, navigate to Shared Nodes and find BNB Smart Chain. GetBlock is an official partner of BSC, and you’ll have an endpoint URL for BSC available (mainnet and testnet options). For the BNB Chain mainnet, the HTTP RPC endpoint is typically: https://go.getblock.io/mainnet/. This is the base URL for JSON-RPC requests.
- Authenticate with API key. For calls to this endpoint, you need to supply your API key in the headers. In HTTP, include a header
x-api-key
whose value is your API key. If you are using cURL or Postman, for example, includex-api-key: YOUR_API_KEY
in the request header.
Quick test. To ensure your API key is working, you can make a simple JSON-RPC call to retrieve the current block number. For example, in Postman or with cURL, use the endpoint and append your API key, then send a JSON payload for the eth_blockNumber
method. The method fetches the block number of the most recent block. The request would be:
POST https://go.getblock.io/mainnet/
Headers: { "x-api-key": "YOUR_GETBLOCK_API_KEY", "Content-Type": "application/json" }
Body: {
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": [],
"id": 1
}
If all is well, the answer will contain a result (in hex) for the current block number. According to GetBlock's documentation, for example, a request to retrieve the latest block number will return the block head of the most recently mined block. You should receive a JSON reply with "id":1
and a "result":"0x"
(block number in hexadecimal). Well done – you're connected to the BNB Chain via GetBlock!
With our API access established, let's make settings for the backend service.
2. Setting Up the Backend Service for Data Polling
I'll create a backend that will call the blockchain periodically for data. You can use Node.js or Python – they both have libraries for calling JSON-RPC requests, or you can simply use HTTP requests. I'll outline the process in Node.js (with Python comments as necessary), but you can use your choice of language.
Project Initialization (Backend)
Create a new directory for your project and set up a Node.js project (or a Python project).
Node.js Setup:
mkdir bnb-analytics-tool && cd bnb-analytics-tool
npm init -y # create package.json with default settings
npm install axios dotenv express web3 ethers # install HTTP client and optional web3 libraries
- We included axios for making HTTP requests (we will make use of this to invoke JSON-RPC endpoints).
- dotenv will also be used to load our API key from an environment file (to keep it secure).
- Express will be used to create a simple API server to serve data to the frontend.
- web3 and ethers (if you want) are libraries that you may use to talk to Ethereum-compatible chains. You can use either of those to make some things simpler, but straight JSON-RPC using axios is fine to learn too.
Create a file backend/.env
to store your GetBlock API key (so you don't hardcode it):
GETBLOCK_API_KEY = your-api-key-here
Create an entry point, e.g., backend/index.js
, and load the API key:
require('dotenv').config();
const axios = require('axios');
const GETBLOCK_KEY = process.env.GETBLOCK_API_KEY;
const BNB_RPC_URL = "https://go.getblock.io/mainnet/";
const axiosConfig = {
baseURL: BNB_RPC_URL,
headers: { "x-api-key": GETBLOCK_KEY }
};
Here we set up the base URL for all BNB Chain RPC calls and include the API key header for authentication on every call. Let's add functions to fetch various on-chain data:
- Wallet balance via
eth_getBalance
- Contract events/logs via
eth_getLogs
- Gas price via
eth_gasPrice
- Transaction volume via
eth_getBlockByNumber
or similar calls
And call each on a periodic schedule.
a. Polling Wallet Balances
To monitor wallet balances, you can use the JSON-RPC method eth_getBalance
. This provides us with the balance (in Wei, the smallest unit of BNB) of an address at a particular block (we typically use the latest block). We'll compile a collection of addresses that we are interested in monitoring (e.g., certain known wallet addresses, or addresses that we have an interest in). Our server will then call eth_getBalance
for each of them at regular intervals and perhaps record the results.
Example. We're interested in monitoring two addresses:
const addressesToTrack = [
"0x1234...ABCD", // replace with actual addresses
"0x5678...EFGH"
];
We can write a function fetchBalances()
to get the balances for all these addresses:
async function fetchBalances() {
const results = {};
for (const addr of addressesToTrack) {
try {
// JSON-RPC request payload for eth_getBalance
const rpcData = {
jsonrpc: "2.0",
method: "eth_getBalance",
params: [addr, "latest"],
id: 1
};
const response = await axios.post("", rpcData, axiosConfig);
// The result is a hex string (balance in Wei)
const balanceWei = response.data.result;
// Convert hex Wei to a readable number (e.g., BNB). 1 BNB = 1e18 Wei.
const balanceBNB = parseInt(balanceWei, 16) / 1e18;
results[addr] = balanceBNB;
} catch (err) {
console.error(`Error fetching balance for ${addr}:`, err);
}
}
return results;
}
In the above code, we post to axios.post("", rpcData, axiosConfig)
– since we set baseURL in axiosConfig
, we can pass an empty string for URL, and axios will call the baseURL. The result response.data.result
is a hex string. We parse it to an integer and then convert from Wei to BNB by dividing by 1e18.
Tip. In a production scenario, you might use a library like web3.js or ethers.js to handle unit conversion and RPC calls more gracefully. For example, using web3:
const Web3 = require('web3');
const web3 = new Web3(BNB_RPC_URL); // assuming GETBLOCK_KEY is embedded in the URL or set in options
const balanceWei = await web3.eth.getBalance(addr);
const balanceBNB = web3.utils.fromWei(balanceWei, 'ether');
But using raw JSON-RPC as above is fine for learning and gives clarity on what’s happening.
Scheduling balance polls. We’ll want to call fetchBalances()
periodically (e.g., every minute) to update the balances. One way to do this in Node is using setInterval
. For example:
// Poll balances every 60 seconds
setInterval(async () => {
const balances = await fetchBalances();
console.log("Updated Balances:", balances);
// TODO: you might store these in a database or in-memory object
}, 60000);
(If working in Python, you might get similar scheduling with schedule
library or a loop with time.sleep(60)
.)
Thus, our backend maintains a current record of the monitored addresses' balances. One actual use case would be to alert when a balance falls below or crosses above a threshold. Indeed, as Chainstack observes, one useful use case for eth_getBalance
is a script that monitors an account's balance at intervals (for instance, to top it up if it falls below some value). In our case, we’ll use it for analytics.
b. Polling Contract Events (Logs)
The BNB Chain (being EVM-compatible) allows querying smart contract past events (logs). It can be useful to monitor some activity, e.g., token transfers, specific contract events (e.g., a DeFi protocol event), NFT minting events, etc.
I use the JSON-RPC method eth_getLogs
to get logs. The method has filter parameters:
address
: the contract address to filter events from (optional; if not specified, matches events from any contract).topics
: list of topics to filter against. The first topic is always going to be the event signature (keccak hash of the event name and types), and others (where indexed in the event) can filter specific values (for example, a specific sender or receiver address in a Transfer event).fromBlock
andtoBlock
: block range of the query. You can provide block numbers or keywords like"latest"
.
Example: Suppose we want to monitor BEP-20 token transfers for some token contract (BEP-20 is analogous to ERC-20 on BNB Chain). We can search for the Transfer(address, address, uint256)
event of the concerned token contract. The event signature hash of an ERC-20 Transfer is a fixed constant: Transfer(address,address,uint256)
hashed (keccak256) of 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
would be the first of two. The second would be sender's address (padded to 32 bytes) and the third receiver's address.
Let’s say our token contract is at address 0xTokenContract...123
. To fetch transfer events from the last 100 blocks, we could do:
async function fetchTokenTransfers(tokenAddress, fromBlock, toBlock) {
const transferTopic = "0xddf252ad1be2c89b69c2b...523b3ef"; // Transfer event signature
const rpcData = {
jsonrpc: "2.0",
method: "eth_getLogs",
params: [{
fromBlock: Web3.utils.toHex(fromBlock),
toBlock: Web3.utils.toHex(toBlock),
address: tokenAddress,
topics: [ transferTopic ]
// We could also add topics[1] or [2] to filter specific from/to addresses if needed
}],
id: 1
};
const res = await axios.post("", rpcData, axiosConfig);
return res.data.result; // an array of log objects
}
The above uses one subject (the event signature) meaning "give me all Transfer events in this contract between these blocks, sender and receiver not relevant". If you were only interested in getting the transfers to a specific address (e.g., your wallet), you would put that address (as a 32-byte hex string) into topics[2]
(because in the Transfer event, indexed to is the second indexed parameter). Similarly, topics[1]
would have been filtered through the sender.
JSON-RPC Example. Below is a typical eth_getLogs
request that might look like this JSON (this one filters through a certain address and certain from/to topics):
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getLogs",
"params": [{
"fromBlock": "0xABCDEF", /* start block in hex */
"toBlock": "0xABCFFF", /* end block in hex */
"address": "0xb59f67a8bff5d8cd03f6ac17265c550ed8f33907", /* contract address */
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", /* Transfer event sig */
"0x00000000000000000000000000b46c2526e227482e2ebb8f4c69e4674d262e75", /* from addr (padded) */
"0x00000000000000000000000054a2d42a40f51259dedd1978f6c118a0f0eff078" /* to addr (padded) */
]
}]
}
Above, I've shown you how to filter logs for a specified sender and receiver of a token contract (the long hex strings with lots of initial zeros are addresses). In this case, you might not need to initialize topics[1] or [2] unless you wish to filter some addresses; initializing just the first topic to the event signature will grab all Transfer events on this contract.
Polling strategy for logs. To check contract events occasionally, you can perform occasional polls, such as balances. For example, call eth_getLogs
for the last N minutes of blocks or since the last block you have checked every N minutes. Be sure to remember the last block that you processed so that there is no overlap or miss:
let lastCheckedBlock = currentBlockNumber - 100; // start 100 blocks ago, for example
setInterval(async () => {
const latestBlockHex = await axios.post("", {
jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 1
}, axiosConfig);
const latestBlock = parseInt(latestBlockHex.data.result, 16);
// Fetch logs from lastCheckedBlock+1 to latestBlock
const logs = await fetchTokenTransfers("0xTokenContract...123", lastCheckedBlock + 1, latestBlock);
logs.forEach(log => {
console.log("New Transfer log:", log);
// TODO: process or store log (e.g., parse values, addresses)
});
lastCheckedBlock = latestBlock;
}, 30000); // run every 30 seconds
In production, consider that BNB Chain has a fast block time (~3 seconds). Polling every 30s (or a minute) should catch events with minimal delay. Be mindful of rate limits (GetBlock free tier allows a certain number of requests per second/day).
c. Polling On-Chain Metrics (Gas Price and Transaction Volume)
Monitoring network-level metrics like gas price and transaction volume can provide insight into network congestion and activity trends.
- Gas Price. On BNB Chain, gas price is typically more stable (and lower) than Ethereum, but it can still vary. We can fetch the current recommended gas price via the
eth_gasPrice
JSON-RPC call. This returns the current gas price in Wei (as a hex string). Alternatively, since BNB Chain now supports EIP-1559 style fee market, you could useeth_feeHistory
to get base fees (if applicable) but gasPrice is simplest for a basic tracker.
Example gas price fetch:
async function fetchGasPrice() {
const rpcData = {
jsonrpc: "2.0",
method: "eth_gasPrice",
params: [],
id: 1
};
const res = await axios.post("", rpcData, axiosConfig);
const gasPriceWei = res.data.result; // hex string
return parseInt(gasPriceWei, 16) / 1e9; // convert Wei to Gwei (1 Gwei = 1e9 Wei)
}
This function returns the gas price in Gwei. We're dividing by 1e9 because 1 Gwei = 1e9 Wei. We can poll it every few seconds or per block. It might not change per block on BNB Chain if gas is constant. Polling every 10 or 15 seconds is fine.
Transaction volume. We need to specify what measure we want for "transaction volume." Usual meanings:
- Number of transactions per block (throughput).
- Frequency of transactions within an interval (minute/hour).
- Total on-chain transactions value (total amount) – while this would normally be totaling values and maybe picking out particular tokens.
For convenience, let's take transaction count per block or per unit time as our measurement. We can get the count of the latest block's transactions and observe it over time to plot a transactions-per-block graph, or total transactions in the previous X blocks for a transactions-per-minute measurement.
Option 1: Use eth_getBlockTransactionCountByNumber
for each block.
async function getTxCountInBlock(blockNumber) {
const rpcData = {
jsonrpc: "2.0",
method: "eth_getBlockTransactionCountByNumber",
params: [ "0x" + blockNumber.toString(16) ],
id: 1
};
const res = await axios.post("", rpcData, axiosConfig);
return parseInt(res.data.result, 16);
}
If you call this with "latest"
as the param instead of a specific block number, it might give the count for the latest block.
Option 2: Fetch the whole block and count transactions:
async function getLatestBlockTxCount() {
const rpcData = {
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [ "latest", false ], // 'false' to not get full tx details to reduce payload
id: 1
};
const res = await axios.post("", rpcData, axiosConfig);
const block = res.data.result;
return block.transactions.length;
}
Here we request the latest block and ask for only transaction hashes (false
for full txn objects). The result’s transactions
array length is the number of transactions in that block.
You could poll this every block or at intervals to record transaction throughput. For example:
setInterval(async () => {
const txCount = await getLatestBlockTxCount();
const gasPrice = await fetchGasPrice();
console.log(`Latest block had ${txCount} transactions, current gas price ~${gasPrice} Gwei.`);
// Store these metrics for the dashboard (e.g., push to an array or database)
}, 10000); // every 10 seconds
By polling every 10 seconds, you most likely capture every new block (BNB Chain ~3s block time, so we should capture 3-4 blocks, perhaps missing some if we're unlucky – you can poll more frequently or through websockets for live, but let's stick to periodic polling).
Data Storage. In the most basic implementation, you might simply store the latest values in RAM or accumulate them in an array (for history in charts). If you need a more powerful solution, keep in mind the utilization of a lightweight database or even a cache. For example, Redis or a time-series database would work if you are going to be storing many older records.
In my tutorial, an in-memory array will suffice in JavaScript to pass the data through to the frontend. So, we have accomplished polling the key data. Next, let’s address the whale tracking module, which builds on some of these concepts.
3. Developing a Whale-Tracking Module (Keeping an Eye on Big Wallets)
"Whales" are transactions with very high balances of cryptocurrency. Their movements (gigantic inflows or outflows) can be an early indicator of direction in the market. As one recent article had it, "Tracking large on-chain transactions is a key trading and research strategy for those who want to predict market movement. Whales tend to move money in large blocks, and a real-time tracking of these transactions might be a valuable source of information."
My tool will include a whale-tracking capability with two main functionalities:
- Whale Wallet Monitoring. Track known whale addresses (e.g., top 10 holders of BNB or any token) – when they make a move, we track or alert it.
- Large Transfer Alerts. Identify any transfer over a defined value threshold, even if we don't have information on the address (e.g., any transfer > 10,000 BNB should be alerted).
In order to accomplish this, we can utilize the methods above:
- Re-use
eth_getLogs
for token transfers (for high token transactions) - Check native BNB transfers by scanning block transactions for high amounts
Identify Whale Addresses. You could manually create a list of addresses that are considered "whales" (e.g., exchange cold wallets, or the wealthiest addresses of a BNB holder list). For instance, let's say we have:
const whaleAddresses = new Set([
"0xWhaleAddress1...",
"0xWhaleAddress2...",
// etc.
]);
const LARGE_TX_THRESHOLD = 1000; // Example threshold: 1000 BNB (adjust as needed)
This means any single transfer >= 1000 BNB we consider noteworthy.
Monitoring Native BNB Transfers. We can examine each new block’s transactions:
async function scanBlockForLargeTx(blockNumber) {
// Fetch block with full transactions to see values
const rpcData = {
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [ "0x" + blockNumber.toString(16), true ], // 'true' to get full tx objects
id: 1
};
const res = await axios.post("", rpcData, axiosConfig);
const block = res.data.result;
if (!block) return;
block.transactions.forEach(tx => {
if (!tx.value) return;
const valueBNB = parseInt(tx.value, 16) / 1e18;
if (valueBNB >= LARGE_TX_THRESHOLD) {
const from = tx.from;
const to = tx.to;
console.log(`🔍 Large BNB transfer detected in block ${blockNumber}: ${valueBNB} BNB from ${from} to ${to}`);
// We can tag if either from/to is in our whaleAddresses list
const fromWhale = whaleAddresses.has(from);
const toWhale = whaleAddresses.has(to);
if (fromWhale || toWhale) {
console.log(` (Whale address involved: ${ fromWhale ? from : to })`);
}
// TODO: Save this info for display or further analysis (e.g., push to an alerts array)
}
});
}
This function fetches a block and iterates through all transactions, checking their value. If the value (converted from Wei to BNB) exceeds our threshold, it logs the transfer and notes if either party is a known whale.
Monitoring Token Transfers. Large moves may also happen in tokens (like large USDT, BUSD, or CAKE transfers). To catch these, we could use eth_getLogs
on popular token contracts for Transfer events, similar to earlier. However, doing this for many tokens can be complex. An alternative approach is to use an analytics API or service. For our custom tool, perhaps we limit to a few known tokens of interest and set thresholds in terms of token amount.
For example, to watch large BUSD transfers:
- BUSD contract address (on BSC).
- Transfer event topic.
- Filter by amount in the log data field.
Token transfer logs include the value transferred in the data
field (for ERC-20/BEP-20, the data
is the amount in hex). We’d have to decode that. If using Web3.js, it can decode event data given the ABI of the event. For simplicity, we might skip detailed token decoding in this tutorial, but it’s good to note.
Scheduling Whale Monitor. We can tie the whale scan to new blocks. For instance, after fetching each new block (like in our transaction volume poll), call scanBlockForLargeTx(latestBlock)
. If we’re already fetching every block in that function, we could integrate it there.
Alternatively, use a separate interval:
let lastScannedBlock = /* last known block */;
setInterval(async () => {
const latestBlockHex = await axios.post("", {
jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 1
}, axiosConfig);
const latestBlock = parseInt(latestBlockHex.data.result, 16);
for (let blk = lastScannedBlock + 1; blk <= latestBlock; blk++) {
await scanBlockForLargeTx(blk);
}
lastScannedBlock = latestBlock;
}, 15000); // check every 15 seconds
This loop catches up from the last scanned block to the newest, scanning each block for large transactions. We use a 15-second interval as a balance between timeliness and not hammering the API (note: scanning each transaction in a block is somewhat heavy; if block frequency is high, consider increasing interval or using websockets to only react to new blocks).
Alerting/Logging. For our tutorial, we’ll just log the events or store them in an array. In a real app, you might send alerts (e.g., email, Telegram, etc.) when a large movement is detected. GetBlock actually has a Webhooks feature to push such events, which could be used instead of polling in advanced setups, but here we stick to manual polling for clarity.
At this stage, our backend service (Node.js) is gathering:
- Latest balances of specified addresses
- Recent token transfer logs (if implemented)
- Latest gas price and transaction counts
- Alerts on large transfers / whale movements
We should structure our backend to expose this data to the frontend. A simple approach is to use an Express server with a few endpoints, or even serve a static JSON. For example, we could have:
GET /api/balances
– returns the latest balancesGET /api/metrics
– returns recent gas price and tx count data (maybe a time series for charts)GET /api/alerts
– returns the list of recent whale alerts
In index.js
(Node), we can add:
const express = require('express');
const app = express();
// In-memory storage for data (could be replaced with database)
let latestBalances = {};
let gasPriceHistory = []; // e.g., array of { timestamp, gasPrice }
let txCountHistory = []; // e.g., array of { timestamp, txCount }
let whaleAlerts = [];
// Populate the above in your polling intervals, for example:
async function updateMetrics() {
const gasPrice = await fetchGasPrice();
const txCount = await getLatestBlockTxCount();
gasPriceHistory.push({ time: Date.now(), gasPrice });
txCountHistory.push({ time: Date.now(), txCount });
// (Keep arrays bounded to last N points to avoid memory issues)
}
setInterval(updateMetrics, 10000);
// Update balances periodically
setInterval(async () => {
latestBalances = await fetchBalances();
}, 60000);
// Whale alerts are appended in scanBlockForLargeTx function (modify it to push to whaleAlerts array)
Now define API routes:
app.get('/api/balances', (req, res) => {
res.json(latestBalances);
});
app.get('/api/metrics', (req, res) => {
res.json({
gasPriceHistory,
txCountHistory
});
});
app.get('/api/alerts', (req, res) => {
res.json(whaleAlerts);
});
// Serve frontend (if built separately, you might skip or adjust this)
app.use(express.static('../frontend/build')); // if React app is built here
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log(`Backend service running on port ${PORT}`));
This is a simple setup: the backend collects data and provides it via a REST API. In production, consider adding caching, pagination (if data gets large), and security (CORS, auth if needed). For now, we assume the frontend will request these endpoints from the same domain (hence serving the built frontend statically for convenience, or you can run frontend separately and set up CORS).
Next, let’s build the frontend dashboard that uses this data.
4. Designing the Frontend Dashboard (React + Chart.js/Recharts)
Our frontend will be a React application that displays the data from the backend in a user-friendly way. We’ll use charts to visualize trends (gas price, transaction count over time) and maybe a table or list for balances and whale alerts.
Setup React Project. In the project root (parallel to the backend
folder), use a React bootstrap like Create React App or Vite:
npx create-react-app frontend # or `npm create vite@latest frontend -- --template react`
cd frontend
npm install chart.js react-chartjs-2 recharts
Here we install two charting options: Chart.js (with the React wrapper react-chartjs-2
) and Recharts. You can use either; Chart.js is a powerful canvas-based library, while Recharts is a React-native SVG chart library. We will illustrate using Chart.js, but you can adapt similarly with Recharts.
Project Structure (Frontend). In frontend/src
, you might structure components like:
components/BalanceTable.js
– to show wallet balances in a table.components/GasChart.js
– to show gas price over time in a chart.components/TxChart.js
– to show transaction volume over time.components/WhaleAlerts.js
– to list recent whale alerts.App.js
– main component to lay out the dashboard, fetch data from backend.
For brevity, we’ll sketch a couple of these components.
Fetching Data from Backend. We can use the browser fetch API or a library like axios to call our backend’s endpoints. The data fetching can be done in a useEffect
hook when the component mounts, or periodically if we want live updating on the frontend (since our backend is already polling, it might suffice to just fetch on an interval or on page refresh).
Alternatively, we can implement server-sent events or web sockets for push updates, but to keep it simple, we’ll poll from the frontend every few seconds to get the latest data (or use a hook that fetches on an interval).
Let’s assume our backend is running on http://localhost:3001
. We might create a small utility for API calls in frontend/src/api.js
:
export async function fetchJSON(path) {
const res = await fetch(`http://localhost:3001${path}`);
if (!res.ok) throw new Error("Network error " + res.status);
return await res.json();
}
Now, in our main App.js
, we fetch the initial data:
import React, { useEffect, useState } from 'react';
import { fetchJSON } from './api';
import BalanceTable from './components/BalanceTable';
import GasChart from './components/GasChart';
import TxChart from './components/TxChart';
import WhaleAlerts from './components/WhaleAlerts';
function App() {
const [balances, setBalances] = useState({});
const [gasData, setGasData] = useState([]); // array of { time, gasPrice }
const [txData, setTxData] = useState([]); // array of { time, txCount }
const [alerts, setAlerts] = useState([]);
useEffect(() => {
// Fetch all data once on mount
async function fetchAll() {
const [bal, metrics, alr] = await Promise.all([
fetchJSON('/api/balances'),
fetchJSON('/api/metrics'),
fetchJSON('/api/alerts')
]);
setBalances(bal);
setGasData(metrics.gasPriceHistory);
setTxData(metrics.txCountHistory);
setAlerts(alr);
}
fetchAll();
// Optionally, set interval to refresh data every 15 seconds:
const interval = setInterval(fetchAll, 15000);
return () => clearInterval(interval);
}, []);
return (
<div className="App">
<h1>BNB Chain Analytics Dashboard</h1>
<BalanceTable balances={balances} />
<div className="charts">
<GasChart data={gasData} />
<TxChart data={txData} />
</div>
<WhaleAlerts alerts={alerts} />
</div>
);
}
export default App;
In the above, we use Promise.all
to fetch all endpoints in parallel for efficiency. We update state variables which will propagate to child components.
Displaying Wallet Balances. In BalanceTable.js
, we can render a simple table:
import React from 'react';
function BalanceTable({ balances }) {
const addresses = Object.keys(balances);
return (
<div>
<h2>Wallet Balances</h2>
<table>
<thead><tr><th>Address</th><th>Balance (BNB)</th></tr></thead>
<tbody>
{addresses.map(addr => (
<tr key={addr}>
<td>{addr}</td>
<td>{balances[addr].toFixed(4)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default BalanceTable;
This will list each tracked address and its balance. We format the balance to 4 decimal places for readability. You can enhance by highlighting changes or sorting by balance, etc.
Charting Gas Price and Transaction Volume. Using Chart.js, we can create a line chart for each. The react-chartjs-2
library provides a <Line>
component that takes a data
object and optional options
.
Example GasChart.js
using Chart.js:
import React from 'react';
import { Line } from 'react-chartjs-2';
import 'chart.js/auto'; // import Chart.js baseline
function GasChart({ data }) {
// Prepare data for chart
const labels = data.map(point => new Date(point.time).toLocaleTimeString());
const values = data.map(point => point.gasPrice);
const chartData = {
labels,
datasets: [{
label: 'Gas Price (Gwei)',
data: values,
borderColor: 'rgba(75,192,192,1)',
fill: false,
tension: 0.1
}]
};
const options = {
scales: {
y: { title: { display: true, text: 'Gas Price (Gwei)' } },
x: { ticks: { autoSkip: true, maxTicksLimit: 10 } }
}
};
return (
<div>
<h3>Gas Price (Gwei)</h3>
<Line data={chartData} options={options} />
</div>
);
}
export default GasChart;
This will render a line chart of gas price over time. We convert timestamps to readable time labels on the x-axis, and plot gas price on y-axis. We set some basic chart options (like disabling fill under the line and adding axis titles).
Similarly, a TxChart.js
can plot the number of transactions per block (or per interval):
function TxChart({ data }) {
const labels = data.map(point => new Date(point.time).toLocaleTimeString());
const values = data.map(point => point.txCount);
const chartData = {
labels,
datasets: [{
label: 'Transactions per Block',
data: values,
borderColor: 'orange',
backgroundColor: 'rgba(255,165,0,0.3)',
fill: true,
stepped: true
}]
};
const options = {
scales: {
y: { title: { display: true, text: 'Tx Count' } },
x: { ticks: { autoSkip: true, maxTicksLimit: 10 } }
}
};
return (
<div>
<h3>Transactions per Block</h3>
<Line data={chartData} options={options} />
</div>
);
}
In this chart, we might use a stepped line or area fill to visualize the transaction counts.
If you prefer Recharts, an equivalent gas price chart could be:
/* Using Recharts */
import { LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';
function GasChart({ data }) {
return (
<div>
<h3>Gas Price (Gwei)</h3>
<LineChart width={500} height={300} data={data}>
<CartesianGrid stroke="#ccc" />
<XAxis dataKey="time" tickFormatter={(time) => new Date(time).toLocaleTimeString()} />
<YAxis dataKey="gasPrice" />
<Tooltip />
<Line type="monotone" dataKey="gasPrice" stroke="#4bc0c0" dot={false} />
</LineChart>
</div>
);
}
Recharts would handle formatting slightly differently (e.g., we might need to format the time inside the XAxis or Tooltip as shown).
Whale Alerts Display. For whale alerts, which we stored as an array of alert objects or strings, we can simply list them:
function WhaleAlerts({ alerts }) {
return (
<div>
<h2>Whale Alerts</h2>
<ul>
{alerts.slice(-10).reverse().map((alert, idx) => (
<li key={idx}>{alert}</li>
))}
</ul>
</div>
);
}
Here we take the last 10 alerts (assuming alerts
array grows, we slice and reverse to show newest first). Each alert could be a string like “Large transfer: 1200 BNB from [addr] to [addr]”.
With these components included in App.js
as shown earlier, our dashboard will show:
- A title, the balances table, two charts, and whale alerts list.
You can add some basic CSS to make it look nicer (e.g., side-by-side charts, monospace font for addresses, coloring whale alerts, etc.).
Run the Application:
- Start your backend:
node index.js
(in thebackend
folder). - Start the React dev server:
npm start
(infrontend
folder).
Open http://localhost:3000
in your browser (if using Create React App default port). You should see the dashboard populate with data after a short delay (for the fetch calls to complete). The gas price and tx charts will update every 15 seconds in our setup, and balances every 60 seconds (you can adjust these intervals to your needs). This achieves a live monitoring effect.
5. Project Structure and Architecture
Organizing your project well makes it maintainable and scalable. Here’s a recommended structure for our blockchain analytics tool:
bnb-analytics-tool/
├── backend/
│ ├── index.js # Entry point for Node.js backend (Express server + polling logic)
│ ├── package.json # Node dependencies (axios, express, web3, etc.)
│ ├── .env # Environment variables (GetBlock API key, etc.)
│ └── services/ # (optional) additional modules for clarity
│ ├── pollBalances.js # functions to fetch balances
│ ├── pollEvents.js # functions to fetch logs/events
│ ├── pollMetrics.js # functions for gas price, tx count
│ └── whaleMonitor.js # functions for whale tracking
└── frontend/
├── public/ # static files (maybe to deploy frontend, etc.)
├── src/
│ ├── App.js
│ ├── api.js # helper for API calls to backend
│ ├── components/
│ │ ├── BalanceTable.js
│ │ ├── GasChart.js
│ │ ├── TxChart.js
│ │ └── WhaleAlerts.js
│ └── App.css # styling
├── package.json # React app dependencies (react, chart.js, recharts, etc.)
└── .env # if needed, e.g., REACT_APP_API_URL for backend URL
In this case, backend and frontend are separated, which is good for the separation of concerns (and you might even host them differently). The backend is tasked with actual blockchain queries (via GetBlock), and the frontend deals with presentation. They talk to each other via HTTP (REST API).
For development convenience, you would likely have the frontend on port 3000 and backend on 3001, and set up a proxy or CORS to allow the requests. In production, you can serve the built frontend files from the backend as static files (as shown with express.static), so all on the same server/domain.
Scaling Considerations:
- Rate limits. Free GetBlock tier has a daily and per-second limit on the number of calls you can make. You can hit these limits when polling too frequently or watching too much. Monitor usage (GetBlock dashboard has stats) and switch to a higher tier or optimize your calls (e.g., combine multiple queries in one JSON-RPC batch call to reduce overhead).
- Data Storage. Our tool keeps data in memory for simplicity. For a production tool, you’ll likely want persistent storage. For example, use a database to store historical metrics (for long-term charts beyond the current session) or to keep a log of whale alerts. A time-series database or even a CSV/JSON log could suffice for moderate data volumes.
- Security. If hosting publicly, secure the backend. At a minimum, do not expose your API key (never expose it to frontend or clients). Our approach keeps it server-side. Also, implement CORS properly if hosting frontend separately, and consider adding basic auth or an API key for your own backend if it's not meant to be public.
As your monitoring needs grow (more addresses, more frequent polls), investigate the use of WebSockets or Webhooks instead of frequent polling. GetBlock offers WebSockets endpoints for subscriptions, and webhooks for event-driven pushes. They might be more efficient than continuous polling.
6. Example JSON-RPC Requests for BNB Chain (GetBlock Endpoints)
Finally, let's summarize some of the JSON-RPC requests we used (with example values) when we were making inquiries to BNB Smart Chain via GetBlock. Each request is a JSON object that is sent via HTTP POST to the GetBlock BSC endpoint with your x-api-key
header appended:
- Get Latest Block Number (
eth_blockNumber
): Retrieves the latest block number.
{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": [],
"id": 1
}
Response: e.g. {"id":1, "jsonrpc":"2.0", "result": "0x1e2403"}
(a hex number, here 0x1e2403
= decimal 1971203).
- Get Balance of an Address (
eth_getBalance
): Returns the balance of a given address in Wei.
{
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": ["0x1234567890abcdef1234567890abcdef12345678", "latest"],
"id": 2
}
The "params"
include the target address and the block identifier ("latest"
for current balance).
Response: e.g. {"id":2, "jsonrpc":"2.0", "result": "0x8ac7230489e80000"}
which is a hex value. 0x8ac7230489e80000
in decimal is 10^20 Wei, or 100 BNB (since 1 BNB = 1e18 Wei).
- Get Logs/Events (
eth_getLogs
): Fetches event logs based on filters.
{
"jsonrpc": "2.0",
"method": "eth_getLogs",
"params": [{
"fromBlock": "0x10D4B8",
"toBlock": "0x10D4BF",
"address": "0x55d398326f99059fF775485246999027B3197955",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
]
}],
"id": 3
}
In this example, we’re fetching logs from block 0x10D4B8 to 0x10D4BF (decimal 1091768 to 1091775) for the address 0x55d398326f...
(this is actually the USDT token contract on BSC) and filtering only Transfer
events (the topic given is the Transfer event signature).
Response: The result will be an array of log objects. Each log object will include things like address
(contract address), topics
(event signature and indexed args), data
(non-indexed data, e.g., amount), blockNumber
, transactionHash
, etc. If no logs match, the result may be an empty array.
- Get Current Gas Price (
eth_gasPrice
): Gets the suggested gas price in Wei.
{
"jsonrpc": "2.0",
"method": "eth_gasPrice",
"params": [],
"id": 4
}
Response: e.g. {"id":4, "jsonrpc":"2.0", "result": "0x09184e72a000"}
. This example hex represents 10000000000000 wei (which is 0.00001 BNB, or 10 Gwei). Convert hex to decimal and then to Gwei for readability.
- Get Block by Number (
eth_getBlockByNumber
): Retrieves a block’s data.
{
"jsonrpc": "2.0",
"method": "eth_getBlockByNumber",
"params": ["0x1e2400", false],
"id": 5
}
Here "0x1e2400"
is the block number in hex (decimal 1971200) and false
means do not include full transaction details (only tx hashes).
Response: You’ll get a block object with fields like number
, hash
, parentHash
, miner
, gasUsed
, transactions
(list of hashes or objects), etc.
- Get Transaction Count in a Block (
eth_getBlockTransactionCountByNumber
):
{
"jsonrpc": "2.0",
"method": "eth_getBlockTransactionCountByNumber",
"params": ["0x1e2400"],
"id": 6
}
Response: e.g. {"id":6, "jsonrpc":"2.0", "result": "0x85"}
– this means 0x85 transactions (hex 0x85 = decimal 133 transactions in that block).
These JSON examples can be tested with any utility (cURL, Postman) by hitting GetBlock's BSC endpoint with your API key. They demonstrate the request structure that our backend is posting in the background. Note that all numeric values are hex strings in Ethereum's JSON-RPC (prefixed with "0x"), and addresses are hex strings of 40 hex digits (20 bytes) prefixed with "0x".
Conclusion and Next Steps
We built a foundational blockchain analytics dashboard for BNB Chain with GetBlock's RPC API. The backend service periodically loads on-chain data (balances, events, metrics) and the React frontend shows the data in nearly real-time. We also incorporated a whale alert system to spot large transactions.
Customization. You can use this tool in different ways:
- Track other metrics (e.g., token prices by the addition of an API, smart contract state changes, pending transactions in mempool if needed, etc.).
- Add additional charts or pages (e.g., a page for the portfolio of one particular address, or a transaction map).
- Improve the UI/UX (add filtering, refresh controls, better responsive design).
- Include whale alert notifications (sound, email, etc.).
- Maintain historical data in a database in order to allow long-term analysis (e.g., daily transaction volume graphs).
As your tool grows, consider using more robust infrastructure: messaging queues for handling data fetch jobs, separating the concerns of data collection and API serving, and possibly deploying on cloud services. If you anticipate very high-frequency data needs, you might need to have a BNB node of your own (or pay for a dedicated node from GetBlock) in order to avoid rate limits.
This tutorial gives you a starting point. You can now observe BNB Chain data and even use the same architecture for other chains (GetBlock supports many networks). Happy coding, and enjoy your new level of insight into the blockchain!