The first four articles in this series covered JSON-RPC's message format, transports, a from-scratch implementation, and the AI/MCP ecosystem. This final article grounds everything in the two largest deployed JSON-RPC systems in the world: the Ethereum blockchain and the Language Server Protocol that powers your IDE.
1. Ethereum: The Billion-Dollar JSON-RPC API
Every interaction with the Ethereum blockchain — checking a balance, sending a transaction, reading a smart contract, subscribing to new blocks — is a JSON-RPC call. This isn't an implementation detail. The Ethereum execution API is formally specified as a JSON-RPC 2.0 interface, and every client implementation (Geth, Besu, Erigon, Nethermind, Reth) must conform to it.
Infrastructure providers like Infura, Alchemy, and Ankr process billions of these JSON-RPC calls daily. When you use MetaMask to send ETH, the wallet constructs a JSON-RPC request, signs the transaction locally, and sends it to a node via eth_sendRawTransaction. When Etherscan displays a contract's state, it's calling eth_call behind the scenes. The entire Ethereum ecosystem — wallets, explorers, DeFi protocols, bridges, L2s — communicates through this protocol.
1.1 Architecture: wallet → node
The architecture maps directly to JSON-RPC's client-server model. The wallet (MetaMask, Trust Wallet, Rabby, or your custom dApp) is the JSON-RPC client. The Ethereum node is the JSON-RPC server. The wallet sends requests, the node processes them and returns responses.
Crucial distinction: JSON-RPC is the edge protocol (Client ↔ Node). It is NOT the network protocol. Ethereum nodes talk to each other using a peer-to-peer protocol called devp2p (over RLP/SSZ). You cannot send JSON-RPC commands to the network itself; you must send them to a specific node, which then propagates the transaction via P2P.
Nodes typically expose JSON-RPC over three transports simultaneously — exactly the three we covered in Article 2:
| Transport | Default port | Use case |
|---|---|---|
| HTTP | 8545 |
Standard queries from dApps and wallets. One request per connection. |
| WebSocket | 8546 |
Subscriptions (eth_subscribe) and multiplexed requests. Persistent connection. |
| IPC | geth.ipc |
Local scripts on the same machine. Ultra-low latency. Also used for consensus ↔ execution layer communication (Engine API). |
Note: The Engine API — the channel where the Consensus Client (Proof of Stake) controls the Execution Client — is also pure JSON-RPC, typically over JWT-authenticated HTTP on port 8551. This is a perfect example of JSON-RPC being used for high-stakes internal microservices communication.
The same method names, the same request/response structure, and the same error codes work identically over all three. This is JSON-RPC's transport agnosticism in production.
1.2 Common Methods
Ethereum exposes hundreds of methods, but most applications rely on a small core set for reading state, sending transactions, and listening for events.
| Method | Type | Purpose & Notes |
|---|---|---|
eth_getBalance |
Read | Get address balance in Wei. Params: [address, blockTag]. Returns a hex string. |
eth_call |
Read | Execute a read-only smart contract call. The data param contains the ABI-encoded selector+args. Costs zero gas (read-only simulation). |
eth_blockNumber |
Read | Get the latest block number. Used constantly by wallets to track chain progress. |
eth_gasPrice |
Read | Get current gas price (estimate). |
eth_sendRawTransaction |
Write | Submit a signed transaction to the mempool. The node never sees the private key; signing happens client-side. |
eth_subscribe |
Sub | Real-time feeds for newHeads (blocks), logs (events), newPendingTransactions or syncing. |
1.3 Batch requests: the performance multiplier
Ethereum is one of the best examples of why JSON-RPC batch support matters. A portfolio tracker displaying 20 token balances for a single wallet would need 20 separate eth_call requests. Without batching, that's 20 HTTP round-trips. With a batch, it's 1. You can try this yourself:
# Send a batch of 3 requests (Block Number, Gas Price, Chain ID) in one HTTP call:
curl -X POST https://ethereum.publicnode.com \
-H "Content-Type: application/json" \
-d '[
{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1},
{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":2},
{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":3}
]'
# Response is an array of 3 results:
# [{"jsonrpc":"2.0","id":1,"result":"0x..."}, {"jsonrpc":"2.0","id":2,"result":"0x..."}, ...]
The response is an array of results, correlated by id. Most RPC providers (Infura, Alchemy) support significant batch sizes (often 100+ via HTTP), making this a critical optimization.
Production Tip
Public endpoints (like https://ethereum.publicnode.com) have strict rate limits and often block large batches or expensive calls like eth_getLogs. For production apps, always use a dedicated provider (Alchemy, Infura, QuickNode, Ankr) with an API key.
1.4 Why JSON-RPC won for Ethereum
Ethereum chose JSON-RPC in 2015, and a decade later it remains the standard despite newer alternatives. The reasons map to the protocol's strengths we've covered throughout this series:
Transport flexibility. A single API specification serves HTTP (dApps and wallets), WebSocket (subscriptions and MEV searchers), and IPC (local tooling and inter-client communication). REST is bound to HTTP. gRPC needs HTTP/2. JSON-RPC works everywhere.
Simplicity. The Ethereum JSON-RPC spec is a flat list of methods. No URL hierarchy to design, no content negotiation, no HATEOAS links. Adding a new method (like eth_feeHistory for EIP-1559) means adding one more entry to the list. Every client library in every language (ethers.js, web3.py, ethclient in Go, ethers-rs in Rust) implements the same simple pattern: serialize request → send → deserialize response.
Batch support. Built into the protocol, not bolted on. A portfolio tracker, a block explorer indexing transactions, a validator checking multiple accounts — they all benefit from native batching.
2. Language Server Protocol: Solving M × N
Before 2016, every IDE needed custom support for every programming language. VSCode needed a Go plugin, a Python plugin, a Rust plugin. Vim needed different ones. Sublime needed different ones again. With M editors and N languages, the industry maintained M × N integrations — each with different quality, different feature sets, and different maintainers.
The Language Server Protocol (LSP), introduced by Microsoft in 2016, solved this with JSON-RPC 2.0. Each language implements one language server (the JSON-RPC server). Each editor implements one LSP client (the JSON-RPC client). Now it's M + N integrations instead of M × N — the same architectural insight that MCP later applied to AI tools.
2.1 Transport: stdio, not HTTP
LSP uses JSON-RPC over stdio (standard input/output), not HTTP. When you open a Go file in VSCode, the editor spawns the gopls process and communicates via piped stdin/stdout. Messages use the Content-Length framing we described in Article 2.
This is the right transport choice because editor and language server are co-located processes on the same machine. There's no need for network overhead, port allocation, or HTTP connection management. Just fast, low-latency pipe communication.
2.2 Where notifications matter most
LSP is the strongest example of why JSON-RPC notifications are not optional. Every time you type a character, the editor sends a textDocument/didChange notification to the language server:
// Sent on EVERY keystroke — no "id", no response expected
{
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": { "uri": "file:///src/main.go", "version": 42 },
"contentChanges": [{ "text": "...full file content..." }]
}
}
If this were a regular request (with id), the editor would have to manage thousands of useless response messages. At 80 WPM, that's ~6 requests per second. Notifications eliminate this overhead entirely — the editor sends the data and immediately forgets about it, with zero round-trip latency. The server processes the change asynchronously and may send diagnostics (errors, warnings) back later as a separate server-to-client notification.
Meanwhile, operations that do need a result use regular requests:
// Hover: request with id — editor needs the type info to display
→ {"jsonrpc":"2.0", "id":15, "method":"textDocument/hover",
"params": {"textDocument":{"uri":"file:///src/main.go"}, "position":{"line":10,"character":5}}}
← {"jsonrpc":"2.0", "id":15, "result": {
"contents": {"kind":"markdown", "value":"```go
func Println(a ...any) (n int, err error)
```"}}}
Autocomplete (textDocument/completion), go-to-definition (textDocument/definition), find-references (textDocument/references), rename (textDocument/rename) — all are request-response. The pattern is consistent: use notifications for high-frequency, fire-and-forget events; use requests when the client needs data back.
See it yourself: VSCode Tracing
You can watch this traffic live in VSCode. Open your settings (JSON) and add "rpc.trace.server": "verbose". For specific languages, use "[lang].trace.server": "verbose" (e.g., "go.trace.server", "python.trace.server"). Then check the Output panel and select your language server from the dropdown. You'll see the raw JSON-RPC messages flying back and forth.
3. Patterns Across All Three Domains
Ethereum, MCP, and LSP are different domains, different scales, different transports. But they chose the same protocol, and they use it in remarkably similar ways:
Namespacing scales. Ethereum has eth_, net_, web3_, debug_. MCP has tools/, resources/, prompts/, notifications/. LSP has textDocument/, workspace/, window/. All three needed to organize hundreds of methods, and all three used the method name string as the organizing principle. No URL hierarchy needed.
Notifications are essential, not optional. Ethereum uses them for subscription pushes (eth_subscription). MCP uses them for progress updates, capability change events, and the initialized signal. LSP uses them on every keystroke.
Transport choice follows architecture. Ethereum uses HTTP for simple dApp queries, WebSocket for subscriptions, and IPC for local node communication. MCP uses stdio for local tools and HTTP for remote servers. LSP uses stdio because editor and server are co-located. Same protocol, different transports, each selected for the deployment context.
Simplicity was the deciding factor. All three protocols could have used REST, gRPC, or a custom binary format. All three chose JSON-RPC because the implementation barrier is nearly zero. A minimal server is 50 lines. Every language has JSON support. There's nothing to compile, no schema language to learn, no code generation step. This low barrier is why each ecosystem grew rapidly.
The JSON-RPC 2.0 spec is 16 years old and roughly 2,000 words long. It has no versioning drama, no breaking changes, no committee politics. It works the same today as it did in 2010. And it quietly powers some of the most important infrastructure in computing: the blockchain networks handling billions in daily value, the AI agents connecting to the real world, and the code intelligence in every major IDE.
If you've read this series, you now understand the protocol at a deeper level. You can build compliant servers and clients, debug wire-level JSON-RPC issues, make informed transport choices, and design tool schemas that LLMs can use reliably.