Blockchains speak JSON at every layer
Whether you're calling a node, reading a smart contract's interface, or checking whether a transaction succeeded, you're working with JSON. Ethereum and most EVM-compatible chains (Polygon, Base, Arbitrum) expose their entire node API as JSON-RPC, describe every smart contract's callable functions as a JSON ABI, and return every transaction's outcome as a JSON receipt. None of it is blockchain-specific magic — it's the same JSON-RPC 2.0 shape used by MCP and other tool protocols, applied to a different domain.
Talking to a node: JSON-RPC
Every Ethereum node — whether run by Infura, Alchemy, or your own — accepts JSON-RPC 2.0 requests over HTTP:
// Request — get the latest block number
{ "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] }// Response — a hex-encoded number, per Ethereum's own convention
{ "jsonrpc": "2.0", "id": 1, "result": "0x1234567" }Note the result is a hex string, not a JSON number — Ethereum's JSON-RPC spec encodes all quantities this way to avoid ambiguity around leading zeros and to support values far beyond JavaScript's safe integer range. Client libraries (ethers.js, web3.js, viem) convert this for you automatically.
// Request — read an account's balance
{
"jsonrpc": "2.0", "id": 2, "method": "eth_getBalance",
"params": ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "latest"]
}The ABI: a contract's JSON interface
A smart contract's Application Binary Interface (ABI) is a JSON array describing every function, event, and error the contract exposes — conceptually the same role an OpenAPI spec plays for a REST API, but for a contract's on-chain functions:
[
{
"type": "function",
"name": "transfer",
"inputs": [
{ "name": "to", "type": "address" },
{ "name": "amount", "type": "uint256" }
],
"outputs": [{ "name": "success", "type": "bool" }],
"stateMutability": "nonpayable"
},
{
"type": "event",
"name": "Transfer",
"inputs": [
{ "name": "from", "type": "address", "indexed": true },
{ "name": "to", "type": "address", "indexed": true },
{ "name": "value", "type": "uint256", "indexed": false }
]
}
]Client libraries read this JSON to know how to encode a function call into the raw bytes a contract expects, and how to decode the raw bytes a contract returns (or the raw event logs it emits) back into typed values:
// ethers.js — the ABI JSON drives both encoding and decoding
import { Contract } from "ethers";
const contract = new Contract(contractAddress, abiJson, signer);
await contract.transfer("0x742d...", 1000000n); // encodes using the ABITransaction receipts: what actually happened
Submitting a transaction doesn't tell you whether it succeeded — for that, you fetch its receipt once mined:
{
"transactionHash": "0xabc123...",
"blockNumber": "0x1234568",
"status": "0x1",
"gasUsed": "0x5208",
"from": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
"to": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
"logs": [
{
"address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000742d35cc6634c0532925a3b844bc9e7595f0be"
],
"data": "0x00000000000000000000000000000000000000000000000000000000000f4240"
}
]
}`status: "0x1"` means success, `"0x0"` means the transaction reverted — this is the single field most application code checks first. logs contains the raw event logs the transaction emitted; a client library uses the contract's ABI to decode topics/data back into the named, typed event fields (like from, to, value for a Transfer event) shown in the ABI example above.
Why hex strings everywhere?
Ethereum values (balances, gas, block numbers) can exceed Number.MAX_SAFE_INTEGER by many orders of magnitude — a token can have 18 decimal places and a total supply in the billions, producing numbers no JSON number can represent without precision loss in JavaScript. Encoding every quantity as a hex string sidesteps that entirely; libraries parse it into a big-integer type (BigInt in modern JavaScript, or a dedicated big-number class) rather than a native number.
Debugging Web3 JSON in practice
- Format raw RPC responses. Node responses are minified by default — paste one into JSONKit's JSON Formatter to read a deeply nested receipt or block object.
- Validate an ABI's shape. A malformed ABI JSON causes cryptic encoding errors — check it parses cleanly with the JSON Validator before debugging further upstream.
- Generate types from a contract's ABI. Paste an ABI into JSON to TypeScript to get a rough type shape for a contract's functions and events as a starting point (dedicated ABI-to-types tools like TypeChain go further, but this is a fast sanity check).