The rewards array in a Solana getBlock response describes lamports credited during that block's processing, not the total fees paid by its transactions. The Fee rewardType is the slice that corresponds to collected transaction fees, but it is split between the leader, the burn, and other recipients, so it will not equal the sum of per-transaction fee fields. To reconcile correctly, sum transaction fee fields, isolate Fee rewardType entries, exclude Rent, Staking, and Voting rewards, and treat any difference as a structural split rather than an error. This guide provides a runnable Node.js script and a results table so you can measure the relationship against your own endpoint.
What the getBlock rewards array actually represents
When you call Solana's getBlock RPC method with rewards enabled, the response includes a rewards array. Each entry is keyed by pubkey and rewardType, and the lamports field is signed because some entries represent debits rather than credits. The array describes lamports credited or debited during that block's processing, which includes the leader's fee reward but also other reward events that are not transaction fees at all.
The Solana getBlock RPC reference and reward object documents rewardType values including Fee, Rent, Staking, and Voting. This means the rewards array is a block-level accounting view, not a transaction-fee ledger. If you treat the sum of rewards as the total fees paid by transactions in the block, you will overcount or undercount depending on which reward types are present.
This distinction matters for block explorers, fee dashboards, and accounting jobs. The Solana RPC providers and endpoints (RPC Assistant) page can help you choose an endpoint that returns full rewards data, but the reconciliation logic must be correct regardless of provider.
- rewards entries are keyed by pubkey and rewardType
- lamports is signed: negative values are debits
- rewardType Fee is the slice corresponding to collected transaction fees
- Rent, Staking, and Voting rewards are not transaction fees
How transaction fees and prioritization fees enter a block
Every Solana transaction declares a fee and may add a prioritization fee. The Solana transaction fees and prioritisation fees documentation describes how these are computed and collected. A block's transactions therefore imply a total fee inflow: sum the fee field from each transaction, and if you have prioritization fee data, add that as well.
However, the rewards array does not simply mirror that sum. The runtime applies a split: part of the fee goes to the leader, part is burned, and other recipients may receive portions. The Fee rewardType entries reflect the leader's portion and any other fee-related credits, not the gross fee inflow. This is why the Stack Exchange question about 'rewardType: Fee lamports' exists: the number does not match the naive sum of transaction fees.
For a deeper look at how transactions are encoded and parsed in getBlock responses, see Solana versioned transactions and getBlock parsing. That page covers the encoding details; this page focuses on the accounting identity.
The accounting identities you can assert
You can assert that the sum of per-transaction fee fields across the block should relate to the Fee rewardType entries, with any difference accounted for by the split between the leader, the burn, and other recipients that the runtime applies. This is a structural relationship, not an exact equality. The difference is expected and should be stable across blocks if the split is consistent.
You must exclude Rent, Staking, and Voting reward entries from the fee reconciliation entirely. These represent different economic events: rent is related to account storage, staking rewards are related to stake activation and deactivation, and voting rewards are related to validator voting. Including them will make your reconciliation meaningless.
A correct reconciliation table should show: total transaction fees, total Fee rewardType lamports, the difference, and an 'unexplained' row for any residual after accounting for known splits. If the unexplained row is consistently zero, your model is complete. If it is not, you may be missing a reward type or a fee component.
- Sum transaction fee fields across all transactions in the block
- Isolate Fee rewardType entries and sum their lamports
- Exclude Rent, Staking, and Voting reward entries
- Compute difference and label any residual as unexplained
Data-quality traps that break reconciliation
The rewards array may be absent or empty rather than a zero array. If you assume it is always present, your script will fail on blocks where rewards are not returned. Always check for the presence of the rewards key and handle null or empty arrays gracefully.
A reward entry's lamports is signed because some entries represent debits. If you drop negative lamports entries, you will overstate the total. The amount is in lamports, so divide consistently when converting to SOL. Also, the response varies with the requested transaction-details encoding and with the commitment level, so a comparison across commitment levels is not a like-for-like comparison.
For guidance on handling timeouts and retries when fetching blocks, see Solana RPC timeouts and retries. For historical data considerations, see Querying Solana historical data over RPC.
- rewards may be absent or empty, not a zero array
- lamports is signed; negative values are debits
- divide by 1e9 consistently for SOL
- transaction-details encoding changes the shape of what you sum
- commitment level changes the response; do not mix levels
Runnable script: fetch one block and reconcile lamport flow
The following Node.js script fetches one block with full transaction details, extracts every per-transaction fee, extracts the rewards array split by rewardType, and prints a reconciliation table with computed differences and a labelled 'unexplained' row. It then repeats for a small block range so you can see whether the difference is stable, structural, or a one-off.
Replace the RPC endpoint with your own. The script uses the standard JSON-RPC interface and does not depend on any provider-specific SDK. Run it against a few blocks to build your own results table.
const https = require('https');
const RPC_URL = 'https://api.mainnet-beta.solana.com';
const START_SLOT = 250000000;
const END_SLOT = START_SLOT + 4;
function rpcCall(method, params) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
const url = new URL(RPC_URL);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
});
});
req.on('error', reject);
req.write(data);
req.end();
});
}
async function reconcileBlock(slot) {
const resp = await rpcCall('getBlock', [slot, {
encoding: 'json',
transactionDetails: 'full',
rewards: true,
maxSupportedTransactionVersion: 0
}]);
if (resp.error) {
console.log(`Slot ${slot}: RPC error`, resp.error.message);
return null;
}
const block = resp.result;
if (!block) {
console.log(`Slot ${slot}: no block`);
return null;
}
let totalTxFees = 0;
for (const tx of block.transactions || []) {
const meta = tx.meta;
if (meta && typeof meta.fee === 'number') {
totalTxFees += meta.fee;
}
}
const rewardsByType = {};
let totalRewards = 0;
for (const r of block.rewards || []) {
const type = r.rewardType || 'Unknown';
rewardsByType[type] = (rewardsByType[type] || 0) + r.lamports;
totalRewards += r.lamports;
}
const feeReward = rewardsByType['Fee'] || 0;
const rentReward = rewardsByType['Rent'] || 0;
const stakingReward = rewardsByType['Staking'] || 0;
const votingReward = rewardsByType['Voting'] || 0;
const otherReward = totalRewards - feeReward - rentReward - stakingReward - votingReward;
const difference = totalTxFees - feeReward;
return {
slot,
totalTxFees,
feeReward,
rentReward,
stakingReward,
votingReward,
otherReward,
difference,
unexplained: difference
};
}
(async () => {
const rows = [];
for (let slot = START_SLOT; slot <= END_SLOT; slot++) {
const row = await reconcileBlock(slot);
if (row) rows.push(row);
}
console.log('slot | totalTxFees | FeeReward | Rent | Staking | Voting | Other | Difference | Unexplained');
for (const r of rows) {
console.log(`${r.slot} | ${r.totalTxFees} | ${r.feeReward} | ${r.rentReward} | ${r.stakingReward} | ${r.votingReward} | ${r.otherReward} | ${r.difference} | ${r.unexplained}`);
}
})();Results table: measure against your own endpoint
Use the script above to populate a results table for your own endpoint and block range. The table should have one row per block and columns for total transaction fees, Fee rewardType lamports, Rent rewardType lamports, Staking rewardType lamports, Voting rewardType lamports, other reward lamports, the difference between total fees and Fee rewards, and an unexplained residual.
Run this across at least 10 blocks to see whether the difference is stable. If the difference is consistently a fixed proportion of total fees, that is the documented split. If it varies, you may be missing a reward type or a fee component. Do not assert measured reward numbers from this article; the values depend on your endpoint, commitment level, and block range.
For provider-specific performance characteristics, refer to your provider's documentation. OnFinality's Solana network page and RPC pricing describe available endpoints and plans, but the reconciliation method is provider-agnostic.
- One row per block; columns for each rewardType and difference
- Run across 10+ blocks to assess stability
- Do not compare across commitment levels
- Document your endpoint and block range for reproducibility
Common failures and how to avoid them
Treating the rewards sum as the fee total is the most common failure. The rewards array includes Rent, Staking, and Voting entries that are not transaction fees. Always isolate Fee rewardType entries before comparing to transaction fees.
Dropping reward entries with negative lamports is another trap. Negative entries are debits and must be included in the sum. Summing fee fields from transactions that failed or were skipped will also distort your total; only include transactions that were actually processed and charged a fee.
Comparing a confirmed-commitment block against a finalised-commitment block is not a like-for-like comparison because the response can differ. Forgetting that the block's transaction list encoding changes the shape of what you sum will break your parser. Re-deriving fees from balances instead of from the transactions is error-prone and unnecessary; the fee field is authoritative.
- Do not treat rewards sum as fee total
- Include negative lamports entries
- Exclude failed or skipped transactions from fee sum
- Do not mix commitment levels
- Use the transaction fee field, not balance deltas
Troubleshooting checklist
When your reconciliation does not match expectations, work through this checklist. First, verify that rewards are present in the response. If the rewards key is missing or empty, your endpoint may not return rewards for that block, or you may need to request them explicitly.
Second, confirm you are summing only Fee rewardType entries for the fee comparison. Third, check that you are including negative lamports. Fourth, ensure you are using the same commitment level for all blocks in your comparison. Fifth, verify that your transaction-details encoding is consistent and that you are reading the fee field from the correct location in the response.
If the unexplained residual is large, consider whether your endpoint returns prioritization fee data separately. The Solana transaction fees and prioritisation fees documentation describes how these are structured. For account and rent mechanics that affect reward types, see Solana account info, rent and token accounts over RPC.
- Check rewards presence and endpoint support
- Sum only Fee rewardType for fee comparison
- Include negative lamports
- Use consistent commitment level
- Verify transaction-details encoding and fee field location
Limitations and assumptions
This reconciliation method assumes that the Fee rewardType entries capture all fee-related credits to the leader and other recipients. If the runtime changes how fees are split or introduces new reward types, the method may need adjustment. It also assumes that the transaction fee field is present and accurate for all transactions in the block.
The method does not account for prioritization fees unless your endpoint returns them separately. It also does not attempt to reconcile rent or staking rewards, which are outside the scope of fee accounting. Results will vary by endpoint, commitment level, and block range, so always document your parameters.
For a broader overview of Solana RPC capabilities, see the OnFinality Learn hub and the API service page. These resources can help you choose the right endpoint for your accounting job.
Next steps: keep parsing and runtime questions separate
Now that you understand the accounting identity, keep the parsing detail on the sibling page and the runtime questions on the account/rent page. For versioned transaction parsing and getBlock encoding, continue with Solana versioned transactions and getBlock parsing. For rent, account storage, and token account mechanics, see Solana account info, rent and token accounts over RPC.
If you need historical block data for backtesting your reconciliation, see Querying Solana historical data over RPC. For production reliability, review Solana RPC timeouts and retries.
To choose an endpoint that returns full rewards data, start with Solana RPC providers and endpoints (RPC Assistant). For pricing and plan details, see RPC pricing.