Skip to content
Blockchain Association of Cameroon
BAC-L1-FOUND-1184 h

APIs, JSON-RPC and node integration

Talking to a chain from code: calls, subscriptions, errors, rate limits, and the discipline of never trusting a single source.

  • Query a node over JSON-RPC and handle its error codes
  • Subscribe to events over WebSocket
  • Design a fallback strategy for a failing endpoint

A chain application does not read the chain directly: it asks a node, most often over JSON-RPC, and believes the answer. This workshop teaches you to write a correct JSON-RPC request, to read its error codes without confusing them with a network failure, to follow new blocks through a WebSocket subscription, and to survive the disappearance of your endpoint. You reuse what you know about accounts, nonces and transactions. This page does not open a real WebSocket to a node (browsers can, but these exercises do not): the online exercises combine generated tasks (write a request, read an error) with diagnoses on logs; the real client is coded in the independent lab.

Lessons 1/4 · 15 min

Asking a node: JSON-RPC request and response

How does a wallet know your balance?

It reads no disk: it sends a question to a node, which answers. JSON-RPC is a very simple protocol for that: "RPC" means remote procedure call (calling a function that runs elsewhere), and the messages are JSON. The version described here is 2.0 (the "JSON-RPC 2.0 specification"). Several families of nodes speak it: Ethereum-style nodes, those built on Substrate, and Bitcoin Core also offers a JSON-RPC interface with its own methods. Method names differ from one ecosystem to another.

In this workshop, the practice node uses made-up names: chain_blockNumber, chain_getBalance, chain_getBlockByNumber. Remember the shape, not the names. Another difference from Ethereum: here quantities (such as a block height) are JSON numbers, whereas Ethereum-style nodes write them as hexadecimal strings.

A request has four fields

{"jsonrpc":"2.0","method":"chain_getBalance","params":["0xAAAA…","latest"],"id":7}
  • jsonrpc: always the string "2.0".
  • method: the name of the procedure.
  • params: the arguments, either a list (by position, as above) or an object (by name). Optional when there is nothing to pass.
  • id: an identifier you choose (number or string). The server copies it into its reply, so you can match each answer to its question when several are in flight.

It is sent in the body of an HTTP POST with the header Content-Type: application/json, to the endpoint's address (<endpoint> below), for example with curl:

curl -s -X POST <endpoint> -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"chain_blockNumber","params":[],"id":7}'

A response has a result OR an error

{"jsonrpc":"2.0","result":"0x1f4","id":7}
{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":7}

A response holds result or error, never both. Here "0x1f4" is a hexadecimal number: 1×256 + 15×16 + 4 = 500. Ethereum-style nodes return most numbers this way, as a hexadecimal string; other ecosystems return JSON numbers. Read your node's documentation and convert explicitly: never add two hexadecimal strings.

A big value (a balance in the smallest unit) often exceeds what a JavaScript number can represent exactly; keep it as a string or a bigint until display. You will see this again in the TypeScript workshop.

Notifications and batches

  • A request without an id is a notification: the server answers nothing. Useful to inform, useless to ask.
  • A batch is an array of requests sent at once; the response is an array, in an order that is not guaranteed: you match by id. Handy to save round trips on a slow connection, but some providers cap the batch size.

What this does NOT guarantee

  • JSON-RPC says nothing about the authenticity of the answer: a node can be wrong, late or lying. Nothing here proves that the announced balance is the chain's.
  • The specification says nothing about HTTP: some servers answer "200" even for a JSON-RPC error. Do not trust the HTTP status alone; look at the body.
  • A chain_getBalance method exists on no real node under that name; go read your own node's method list.

Recap: four fields in, result OR error out, id to match.

Lessons 2/4 · 15 min

Reading errors, deciding whether to retry

Three kinds of failure not to confuse

Your code sends a request and does not get what it wanted. Three very different situations.

  1. The transport fails: no response, timeout, connection dropped. You do not even know whether the server received the request.
  2. HTTP reports a problem: for example 429 (too many requests, you exceed the rate limit) or a 5xx code (the server is in trouble).
  3. JSON-RPC answers with an error: the server did receive and understand that something is wrong. The error field holds a code (number) and a message (text for humans).

A program decides on the code, never by parsing the message text.

The standard codes

CodeNameMeaning
-32700Parse errorthe server could not parse the JSON received
-32600Invalid Requestvalid JSON, but not a valid JSON-RPC request
-32601Method not foundthe method does not exist or is not available
-32602Invalid paramsthe parameters are invalid
-32603Internal errorinternal JSON-RPC error

In addition, the specification reserves the range -32000 to -32099 for implementation-defined "server" errors. Codes in that range, and those outside the standard range ("insufficient funds", "nonce too low"…), differ from one node software to another: do not memorise them, read your node's documentation and test them.

What to do with each error?

SituationRetry?Why
Timeout, dropped connectionyes, with waitingprobably a passing outage
HTTP 429 (or a provider-specific JSON-RPC error for a rate limit, sometimes returned with HTTP 200)yes, after the requested waitthe Retry-After header, if present, says how long to wait
HTTP 5xxyes, a few timesserver in trouble
-32700, -32600, -32601, -32602noit is a bug in your request: sending it again identically will fail identically (unless your node's documentation says otherwise: some answer -32602 for a block not yet available or pruned state)
-32603 or a server errorwith caremay be passing; limit the number of attempts

Retrying a read, retrying a send

Reading a balance twice is harmless: a read is said to be idempotent. Sending a transaction again is more delicate, but your knowledge of the nonce helps: a signed transaction has a fixed hash and a fixed nonce. If you send it again, the network will not execute it twice. The node will rather answer "already known" or, if it is already included, "nonce too low". These replies are not failures: look the transaction up by its hash before concluding. By contrast, re-signing a new transaction with a fresh nonce on every attempt can create two payments: avoid it.

A worked example

A client successively receives:

  1. {"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params"},"id":3}: it forgot the second parameter. No new attempt; fix the code.
  2. HTTP 429 with Retry-After: 4: wait at least 4 seconds.
  3. No answer for 10 seconds: give up this attempt (the timeout), then retry after a growing wait.

What this does NOT guarantee

  • The absence of an error is not proof of correctness: the node can answer without error and be wrong.
  • An error code tells what the node saw; it does not tell what the chain will decide. A transaction accepted into the pool can still be dropped later.
  • No node's table of codes applies to another.

Recap: classify the failure first (transport, HTTP, JSON-RPC), then decide to retry or to fix.

Lessons 3/4 · 15 min

Subscribing to blocks over WebSocket

Ask every two seconds, or be told?

A block arrives, say, every 12 seconds. If your application asks "what is the height?" every 2 seconds, it sends 6 requests per block, 5 of which bring nothing new. On a limited connection, that wastes data allowance, and you risk the provider's rate limit.

A WebSocket is a connection that stays open after an initial HTTP exchange: server and client can send each other messages whenever they like. Many nodes offer subscriptions on it: you say once "tell me about every new block header", and the server pushes each event. On Ethereum-style nodes this is the eth_subscribe method with the topic "newHeads"; names vary by ecosystem.

A simplified subscription

The messages below are simplified and schematic: consult your node for the exact shape.

-> {"jsonrpc":"2.0","method":"<subscribe method>","params":["newHeads"],"id":1}
<- {"jsonrpc":"2.0","result":"0x9a","id":1}
<- {"jsonrpc":"2.0","method":"<notification method>","params":{"subscription":"0x9a","result":{"number":"0x64","hash":"0xaa…","parentHash":"0x99…"}}}

The first reply gives the subscription identifier. The following messages have no id: they are notifications, carrying the subscription identifier to say which subscription they belong to.

What happens when the connection drops

This is the heart of the job. A WebSocket can die without warning (mobile network change, an intermediate box that cuts idle connections). And above all:

  • No catch-up. A subscription does not replay what you missed. After reconnecting, you must subscribe again and fill the gap with ordinary requests.
  • Silence is ambiguous. No message for 40 seconds: no new block, or a dead connection? Set a timeout (for example two or three times the expected block interval) after which you consider the connection suspect and reopen it.

A worked catch-up example

You have processed blocks up to number 101. The connection drops, you reconnect, you subscribe again, and the first header received carries number 105. Blocks 102, 103 and 104 are missing: 105 − 101 − 1 = 3 blocks. You request them one by one (or in a batch) with the block-reading method, then process 105.

Reorganisations

The headers you receive can be replaced: two different blocks numbered 105, each with its hash, arrive one after the other. That is a chain reorganisation (you saw forks in the consensus workshops). A serious client keeps, for each block, its number, its hash and its parent's hash, and checks that the new block's parent is the one it stored at the previous number. Otherwise it walks back to the common point and undoes what it had recorded beyond it. This precaution assumes a chain with probabilistic finality (the case of proof-of-work chains and many others); on a chain with instant finality (some BFT protocols), a decided block never changes and this case does not arise.

This is why one waits for some number of confirmations (blocks built on top) before acting on money. The number depends on the chain and on what you risk; no universal value exists.

A reconnection loop (sketch)

async function follow(state: { lastDone: number }) {
  while (true) {
    let socket;
    try {
      socket = await connect();                // opens the WebSocket
      await subscribeToHeads(socket);          // subscribe again each time
      const head = await firstHead(socket);
      for (let n = state.lastDone + 1; n < head.number; n++) {
        await handleBlock(await getBlock(n));  // fill the gap
        state.lastDone = n;                    // progress recorded block by block
      }
      await handleBlock(head);
      state.lastDone = head.number;
      await consume(socket, state);            // updates state.lastDone at each block
    } catch (e) {
      await waitBeforeRetry();                 // see the next lesson
    } finally {
      socket?.close();                         // always close the old connection
    }
  }
}

Progress is recorded block by block in state: if consume throws midway (the usual case of a dropped connection), the next turn restarts from the last block actually processed, without replaying dozens. Even so, a block can still arrive twice (for example if the drop happens between processing and updating state): handleBlock must therefore be idempotent, that is, give the same result if it processes the same block twice.

What this does NOT guarantee

  • A subscription guarantees neither completeness (messages may be missing) nor uniqueness (a block may arrive twice after a catch-up).
  • A received header is not final.
  • The WebSocket does not authenticate the data: same remark as for HTTP.

Recap: subscribe, detect silence, subscribe again, fill the gap, check parents.

Lessons 4/4 · 20 min

Surviving outages, never trusting a single source

Your endpoint will go down

An RPC provider has outages, maintenance windows, rate limits. Your application must plan for that day. Four tools.

1. Wait longer and longer, with randomness

Retrying immediately and endlessly makes the outage worse. We use exponential backoff: the wait doubles after each failure up to a cap. Example with a base of 0.5 s and a cap of 8 s: 0.5 → 1 → 2 → 4 → 8 → 8 → 8… seconds. We add randomness (jitter): wait a duration drawn at random between 0 and the computed value. Why? If 1,000 clients lose the connection at the same instant and all retry exactly on the same second, they rebuild together the spike that saturated the server.

2. Always a timeout and a bounded number of attempts

A request without a timeout can wait forever. Set a timeout (say 10 s), a maximum number of attempts, and at the end show a clear state to the user. A circuit breaker takes the idea further: after K failures in a row on an endpoint, stop calling it for a rest period and move on to the next.

3. Several endpoints, and a freshness check

Keep a list of providers (or a node of your own plus a provider) and switch between them. But an endpoint that answers is not necessarily up to date. Example: A reports height 1,000,120, B 1,000,121, C 1,000,050. C is 71 blocks behind: it answers without error, with stale data. A serious client compares heights and sets C aside.

4. Never trust a single source for an important decision

To credit a deposit or trigger a payment, ask two independent providers: request the hash of the same block (at a sufficient confirmation depth) and compare. If they agree, confidence rises; if they differ, do not act, and ask a third or wait. Honest limits: two providers may depend on the same upstream infrastructure, and agreement does not replace the cryptographic verification offered by a full node or a light client that you run yourself.

Rate limits

When the provider answers 429, honour Retry-After. Reduce your consumption: cache what does not change (an old block no longer changes), use batches, prefer subscriptions to polling, and do not launch hundreds of requests in parallel.

Privacy: the provider sees everything

An RPC provider sees your IP address, the list of addresses you look up, the methods called and the time. If your wallet shows five accounts, the provider learns that those five addresses are linked, and can tie them to your IP and to an identity. HTTPS (TLS) protects the path against intermediate observers; it hides nothing from the provider itself, since it is the one terminating the connection.

Mitigations, none perfect: run your own node; spread requests across several providers so none sees all your addresses; go through a relay that hides the IP; look up only what is necessary. Another point: an API key placed in the code of a web page is public, since every browser downloads it. It identifies an account, it does not protect a secret; limit it on the provider side (allowed domains, rate cap) or go through your own server.

What this does NOT guarantee

  • More providers does not make an answer true: it lowers the probability of believing an isolated error.
  • Randomness and backoff reduce the load of retries, they do not repair the outage.
  • A local node removes the privacy leak toward a third party, but not your own network exposure.

Recap: backoff with jitter, bounded timeouts, several compared sources, and awareness of what each provider sees.

Guided lab

What the browser can do here: make you write a real JSON-RPC request and read a real error, with data generated for you and graded by the server, then analyse two client logs. What it cannot do: open a WebSocket to a node or kill an endpoint. The resilient client, tested by deliberate outage, is the independent lab. The practice node uses fictitious methods (chain_*), whose names appear in each task's statement.

A browser is no substitute for a real terminal: this lab trains reading and reasoning; the real hands-on practice is in the independent lab.

A certified account is required

Graded activities record results in your name and are the basis for a credential. They therefore need a complete certified file and a current consent. Filling in the file does not verify your identity; the Secretariat can verify it afterwards ("Verified" badge).

  1. 1.Write a JSON-RPC request

    A generated task describes a call (balance, block or height) with the method and the id to use. Write the complete JSON body of the request in the field provided: the four fields from lesson 1 (jsonrpc, method, params if needed, id), with no extra field. Mind the order and type of parameters: a number is not a string, and false is not "false". Check your JSON before sending: an extra comma would give a -32700 error on a real node.

  2. 2.Read a JSON-RPC error

    A generated task displays a node's response containing one of the five standard error codes (-32700, -32600, -32601, -32602, -32603). Choose the exact meaning of that code from the list. Reminder from lesson 2: decide by the code, not by the text. Before answering, also ask yourself what your client would do: retry, or fix the request?

  3. 3.Fill the gap after a reconnection

    Log of a client following new blocks (times and numbers are fictitious):

    12:00:01 subscribed to headers (id=0x9a)
    12:00:13 header 100 processed
    12:00:25 header 101 processed
    12:00:37 (no message)
    12:01:05 silence > 40 s: connection judged suspect, closed
    12:01:07 reconnect, subscribe again (id=0x9b)
    12:01:11 first header received: 105

    Answer the question in the answer grid: how many blocks must the client fetch with ordinary requests before processing block 105?

  4. 4.Compute a retry wait

    A client uses exponential backoff without randomness for simplicity. Its rule: the first retry waits 1 s, each following retry waits double the previous one, with a cap of 10 s.

    retry 1: wait 1 s
    retry 2: wait 2 s
    retry 3: wait 4 s
    …

    Answer the question in the answer grid: how many seconds does the client wait before the 5th retry?

Security exercise — What the RPC provider sees

A BAC mobile wallet displays the balances of five of the user's accounts. To do so, at startup, it sends five balance requests, one after the other, to the same public RPC provider, from the same IP address. The provider's logs are kept for three months. On the home page, the provider's API key is written in plain text in the JavaScript code.

Here is what five (fictitious) log lines show the provider:

2026-10-12 08:00:01  IP 203.0.113.9  chain_getBalance  0xAAAA…01
2026-10-12 08:00:01  IP 203.0.113.9  chain_getBalance  0xAAAA…02
2026-10-12 08:00:02  IP 203.0.113.9  chain_getBalance  0xAAAA…03
2026-10-12 08:00:02  IP 203.0.113.9  chain_getBalance  0xAAAA…04
2026-10-12 08:00:02  IP 203.0.113.9  chain_getBalance  0xAAAA…05

(The IP address is a documentation address, not a real one.) The provider is not accused of malice: the subject is what it CAN learn, and what a retained log or a leak can expose to a third party.

Answer-grid question: which consequence is accurate, and which mitigation actually reduces the leak?

Workshop quiz

Eight questions drawn from a larger pool, 70% to pass. It opens once every lab checkpoint is passed. You can retake it: the draw changes.

A certified account is required

Graded activities record results in your name and are the basis for a credential. They therefore need a complete certified file and a current consent. Filling in the file does not verify your identity; the Secretariat can verify it afterwards ("Verified" badge).

Independent lab (outside the site)

Not graded online: do it on your own machine and publish it to your Git repository.

Do this on your own machine, outside the site. Not graded online; assessed by deliberately killing the endpoint.

  1. In TypeScript on Node.js (or the language of your choice), write a client that: connects to a test node over WebSocket, subscribes to new headers and prints number, hash and parent hash of each block. Choose the libraries by reading their documentation and note their versions in your README: this course imposes none.
  2. Add silence detection (a timeout of two or three times the block interval), reconnection with exponential backoff and jitter, resubscription, and catching up on missed blocks with ordinary requests.
  3. Keep number, hash and parent of each processed block; detect a reorganisation (unexpected parent) and log it.
  4. Test: start a local node (see the Docker workshop), stop it with docker compose stop for one minute, restart it, and show in the log the gap filled with no missing block and no duplicate.
  5. Extend the client: ask two RPC providers (or a node of your own and a provider) for the hash of an older block a few confirmations deep, and raise an alert if they differ.
  6. For privacy, write ten lines in the README: what each provider sees of your client and the mitigation you chose.

Takeaways

  • A JSON-RPC 2.0 request has four fields (jsonrpc, method, params, id); the response holds a result OR an error, and the id matches them.
  • Decide by the error code, never by the text: -32602 gets fixed, a timeout gets retried.
  • A WebSocket subscription replays nothing: after a drop, subscribe again and fill the gap, checking parents because of reorganisations.
  • Exponential backoff with jitter, bounded timeouts and attempts, several endpoints whose freshness you check.
  • For an important decision a single source is not enough; and the provider sees your IP and every address you look up.

Common misconceptions

  • Misconception : If the server answers HTTP 200, the request succeeded.

    In fact : Many servers return 200 with an error field in the body. You must read the JSON-RPC body: result or error.

  • Misconception : On error, just resend the same request until it goes through.

    In fact : A params or method error (-32602, -32601) fails every time; and immediate endless retries worsen an outage. Retry only passing failures, with a growing wait.

  • Misconception : With HTTPS, my RPC provider cannot know which addresses I look up.

    In fact : HTTPS protects the path against intermediaries. The provider terminates the connection: it reads every request, with your IP.

Glossary

JSON-RPC
Remote procedure call protocol whose messages are JSON: a request with method, params and id; a response with result or error.
Endpoint
Address at which a node or provider accepts RPC requests (HTTP or WebSocket).
Exponential backoff
Retry strategy where the wait doubles after each failure up to a cap, often with jitter to desynchronise clients.
Idempotent
Said of an operation that can be repeated without changing the outcome beyond the first execution, such as reading a balance.
Confirmations
Number of blocks built on top of the block containing a transaction; the larger it is, the less likely a reorganisation.