HTTP RPC in practice: poking the network yourself
Talking to Solana directly over HTTP JSON-RPC — the same calls every wallet and explorer makes under the hood.
Tooling’s set up, you have a wallet, the concepts are in your head. Time to finally talk to the network directly — over HTTP JSON-RPC. If you’ve never met JSON-RPC: it’s a dead-simple protocol where every request is a JSON object with a method name and params, POSTed to a single endpoint, and every response is a JSON object with a result. No REST-style routes, no verbs — one URL, many methods. A small motivational insight before we start: the entire Solana CLI, every wallet like Phantom, and every explorer (the block-browsing websites where people look up transactions) call exactly the same methods under the hood that we’re about to call with curl. There is no other magic.
Every request is a plain POST with a JSON body to your RPC node — a node that holds a live copy of the chain’s state and answers queries about it. I use Helius; substitute your own URL:
export RPC="https://mainnet.helius-rpc.com/?api-key=<your-key>"
And keep the commitment levels from part 1 in mind: almost every method accepts a commitment parameter (processed / confirmed / finalized) — how settled the state has to be before you’re willing to read it, from “a node has seen it” to “practically irreversible”.
Checking a balance: getBalance
curl -s $RPC -X POST -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0", "id": 1,
"method": "getBalance",
"params": ["8dBTPrjnkXyuQK3KDt9wrZBfizEZijmmUQXVHpFbVwGT", {"commitment": "confirmed"}]
}'
{ "jsonrpc": "2.0", "result": { "context": { "slot": 312545678 }, "value": 1500000000 }, "id": 1 }
The balance comes back in lamports — the smallest unit of SOL, one billionth of it (1 SOL = 1,000,000,000 lamports), so here that’s 1.5 SOL. Note context.slot — the network always tells you as of which slot the answer was given. The solana balance command makes exactly this request.
Reading a whole account: getAccountInfo
getAccountInfo is the main read method. Let’s query your own wallet — that string in params is your public key, encoded in base58 (Bitcoin’s old alphabet: no 0, O, I, or l, so addresses survive being read out loud). Solana uses it for every address and signature you’ll see:
curl -s $RPC -X POST -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0", "id": 1,
"method": "getAccountInfo",
"params": ["<your-address>", {"encoding": "jsonParsed"}]
}'
The response shows every account field from the previous article, live: lamports, owner (the program — Solana’s word for a smart contract — that’s allowed to modify this account; for a plain wallet that’s the System Program, 11111111111111111111111111111111), data (the account’s payload bytes — empty for a wallet), executable: false.
Now query your ATA with the same method — the associated token account, the separate little account that holds your balance of one specific token — and you’ll see the difference: the owner is now the Token Program, and in data (which the node kindly parses from raw bytes into JSON thanks to encoding: "jsonParsed") sit the mint, the owner, and the token amount. One method, and the whole account model is right in front of you.
All of a wallet’s tokens: getTokenAccountsByOwner
You don’t have to know your ATA addresses by heart:
curl -s $RPC -X POST -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0", "id": 1,
"method": "getTokenAccountsByOwner",
"params": [
"<your-address>",
{"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
{"encoding": "jsonParsed"}
]
}'
Back come all token accounts belonging to the wallet. Instead of programId you can filter by a specific mint — the account that defines a token itself (its supply, decimals, who can print more); every token’s “ID” is its mint address. This is exactly how wallets draw your token list.
Transaction history and dissection: getSignaturesForAddress + getTransaction
First we get the list of signatures for transactions the address took part in:
curl -s $RPC -X POST -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0", "id": 1,
"method": "getSignaturesForAddress",
"params": ["<your-address>", {"limit": 5}]
}'
A signature is literally the transaction’s cryptographic signature, and it doubles as the “transaction ID” you see in explorers. Let’s take the signature of our token purchase and pick it apart:
curl -s $RPC -X POST -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0", "id": 1,
"method": "getTransaction",
"params": ["<signature>", {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0}]
}'
The response shows everything we’ve talked about: the list of accounts, the instructions (which program called what — there they are, CPIs, programs invoking other programs mid-transaction!), balances before and after, compute units consumed (the network’s gas-like measure of execution work), and the fee.
Building a transaction by hand: getLatestBlockhash → simulateTransaction → sendTransaction
The full lifecycle of sending looks like this:
getLatestBlockhash— grab a fresh blockhash. It’s the hash of a recent block that you bake into your transaction, serving as proof of freshness and as deduplication (remember: it expires after ~150 blocks, roughly a minute);- build and sign the transaction (this part is no longer curl territory but an SDK —
@solana/web3.jsorsolana-py; I’ll show it in a separate article with code); simulateTransaction— dry-run the transaction. For free, you learn whether it will succeed, how many CU it will eat, and what it writes to the logs. Never skip this step: simulation is your main debugging tool;sendTransaction— send the signed transaction into the network, serialized as base64 (the same base64 you know from everywhere else — signed transactions are binary blobs, unlike base58 addresses, which are meant for human eyes);- poll the status via
getSignatureStatusesuntil you see the commitment level you need.
The important thing to understand: sendTransaction returns the signature immediately, without waiting for the transaction to land in a block. “The method returned a signature” ≠ “the transaction went through”. The node merely forwards it to the leader — the validator whose turn it is to produce the current block (that same Gulf Stream mechanism from part 1) — and retries for a while.
Closing the loop: getLeaderSchedule
And to finish — the method where the whole series started:
curl -s $RPC -X POST -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0", "id": 1,
"method": "getLeaderSchedule"
}'
Back comes that very leader schedule for the current epoch (a span of ~432,000 slots, about two days, for which the rotation is fixed in advance): validator identities and the indices of the slots in which they’ll produce blocks. Nearby live getSlot, getEpochInfo, and getVoteAccounts — with those you can explore for yourself how stake (the SOL locked behind each validator as its voting weight) is distributed across the network and who gets to be leader how often. The more stake, the more slots in the schedule. Check it yourself.
What’s next
HTTP methods answer the question “what’s happening in the network right now, at the moment I asked”. But constantly polling the network in a loop is expensive, slow, and ugly. In the next article we switch to the push model: WebSocket subscriptions, where the network tells you about changes itself. It’s the last stop before the world of data streams that this whole series started from.