7Block Labs
Blockchain Development

ByAUJay

Blockchain API Tutorial for Product Teams: From cURL to Production SDKs

A practical, decision‑maker’s guide to moving from raw JSON‑RPC calls to reliable, observable, multi‑chain SDKs that ship. You’ll learn the exact requests to prototype with cURL, then how to harden them for production across Ethereum (incl. blobs and AA) and Solana, with modern wallet and observability patterns.


Who this guide is for

  • Product, platform, and engineering leaders at startups and enterprises exploring blockchain integrations
  • Teams that have tinkered with RPC calls or SDKs and now need a stable, observable, and scalable path to production

What you’ll build by the end

  • A set of cURL “smoke tests” you can run against any RPC
  • A minimal, production‑ready SDK layout (Node/TypeScript focus, with Python/Go pointers)
  • Robust patterns for fees, retries, websockets, wallet discovery, account abstraction (ERC‑4337), and Solana priority fees
  • An observability plan that traces JSON‑RPC with OpenTelemetry, not just logs

1) Know your RPC surface area (2025 reality)

At minimum, every EVM execution client exposes the JSON‑RPC methods you’ve seen: read calls (eth_call), send (eth_sendRawTransaction), fee estimation (eth_estimateGas/eth_feeHistory), and subscriptions (eth_subscribe over WebSocket). Ethereum.org’s JSON‑RPC page is the canonical reference your team should standardize on. (ethereum.org)

What’s new since “legacy gasPrice days”:

  • EIP‑1559 fee fields on type‑2 transactions: maxFeePerGas and maxPriorityFeePerGas, plus effectiveGasPrice in receipts. Your API surface must set and validate these consistently. (eips.ethereum.org)
  • EIP‑4844 “blob” transactions added fields like maxFeePerBlobGas and blobVersionedHashes and changed block headers (blob_gas_used, excess_blob_gas). If you index or submit L2 data‑availability posts, your API objects and signing must handle blobs. (eips.ethereum.org)
  • Pub/sub is WebSocket‑only: eth_subscribe for newHeads, logs, and pending transactions. Plan for WS URLs, auto‑reconnect, and resubscribe behavior. (alchemy.com)

On Solana, the surface is HTTP+WebSocket JSON‑RPC, but fee control is via Compute Budget instructions (CU limit, CU price in micro‑lamports per CU) and you should dynamically price with getRecentPrioritizationFees. Also understand commitment levels (processed/confirmed/finalized) for read consistency. (solana.com)


2) Prototype with cURL: your “RPC smoke tests”

Start with four requests you can paste into any environment (replace URL/keys).

  • Get latest block number (sanity check)
curl -s "$RPC_URL" -H "content-type: application/json" \
  --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'
  • Read‑only call (ERC‑20 balanceOf)
curl -s "$RPC_URL" -H "content-type: application/json" \
  --data '{
    "jsonrpc":"2.0","id":2,"method":"eth_call",
    "params":[{"to":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","data":"0x70a08231000000000000000000000000<ADDRESS_40_HEX>"},"finalized"]
  }'

Use the block parameter (“finalized”/“safe”/“latest”) consistently in reads that feed user balances and dashboards. (ethereum.org)

  • EIP‑1559 fee envelope
curl -s "$RPC_URL" -H "content-type: application/json" \
  --data '{"jsonrpc":"2.0","id":3,"method":"eth_feeHistory","params":[5,"latest",[25,50,95]]}'

Use fee history to bound maxPriorityFeePerGas and maxFeePerGas; never hardcode. (eips.ethereum.org)

  • WebSocket subscription (blocks → wscat)
wscat -c "$WS_URL"
# then send
{"id":1,"jsonrpc":"2.0","method":"eth_subscribe","params":["newHeads"]}

Your production WS client must auto‑reconnect, resubscribe, and backfill missed ranges via eth_getLogs. (alchemy.com)

Solana fee probe (optional during POC):

curl -s https://api.mainnet-beta.solana.com -H "content-type: application/json" \
  --data '{"jsonrpc":"2.0","id":1,"method":"getRecentPrioritizationFees","params":[]}'

Use returned samples to pick microLamports per CU; don’t guess. (solana.com)


3) From cURL to an SDK you can ship

Here’s a minimal TypeScript shape that scales:

  • One interface per chain (EVM/Solana) with a typed request pipeline
  • Policy middlewares: retries with jitter, circuit breaker, request IDs, timeouts
  • Observability: OpenTelemetry spans for every JSON‑RPC call (rpc.system=jsonrpc)
  • Dual transports: HTTP for reads/batching; WebSocket for subscriptions

Example: EVM client with batching, backoff, OTel, and fee helpers (Viem or Ethers v6 work; Viem shown):

// pkg/evmClient.ts
import { createPublicClient, http, webSocket } from 'viem'
import { mainnet } from 'viem/chains'
import pRetry from 'p-retry'
import { context, trace, SpanStatusCode } from '@opentelemetry/api'

const httpUrls = [process.env.RPC1!, process.env.RPC2!]
const wsUrl = process.env.RPC_WS!

export function makeEvmClient() {
  // Simple endpoint rotation
  let i = 0
  const transport = http({
    url: () => new URL(httpUrls[i = (i+1) % httpUrls.length]),
    timeout: 10_000, // ms
    batch: { batchSize: 20, wait: 10 }, // JSON-RPC batch for read fanout
  })

  const client = createPublicClient({ chain: mainnet, transport })
  const wsClient = createPublicClient({ chain: mainnet, transport: webSocket(wsUrl) })

  async function withSpan<T>(method: string, fn: () => Promise<T>) {
    return await trace.getTracer('rpc').startActiveSpan(method, async (span) => {
      span.setAttribute('rpc.system', 'jsonrpc')
      span.setAttribute('rpc.method', method)
      try {
        const res = await pRetry(fn, { retries: 3, factor: 1.7, minTimeout: 200, maxTimeout: 1500, randomize: true })
        span.setStatus({ code: SpanStatusCode.OK })
        return res
      } catch (e: any) {
        span.setStatus({ code: SpanStatusCode.ERROR, message: e?.message })
        // If provider returns JSON-RPC error, record code/message
        if (e?.code) span.setAttribute('rpc.jsonrpc.error_code', e.code)
        if (e?.message) span.setAttribute('rpc.jsonrpc.error_message', e.message)
        throw e
      } finally {
        span.end()
      }
    })
  }

  return {
    feeHistory: (blocks = 5) =>
      withSpan('eth_feeHistory', () => client.request({ method: 'eth_feeHistory', params: [blocks, 'latest', [25, 50, 95]] })),
    call: (tx: any, tag: string = 'latest') =>
      withSpan('eth_call', () => client.request({ method: 'eth_call', params: [tx, tag] })),
    sendRawTx: (raw: `0x${string}`) =>
      withSpan('eth_sendRawTransaction', () => client.request({ method: 'eth_sendRawTransaction', params: [raw] })),
    onNewHeads: (cb: (header: any) => void) =>
      wsClient.transport.subscribe({ method: 'eth_subscribe', params: ['newHeads'] }, cb),
  }
}
  • The OTel attributes match the JSON‑RPC semantic conventions, so you get method‑level histograms, error codes, and endpoint tags in your APM out of the box. (opentelemetry.io)
  • Batch reads for lists/index pages: geth implements JSON‑RPC batching; use it to slash latency. (geth.ethereum.org)

Python teams can mirror this with httpx + tenacity + opentelemetry‑instrumentation‑httpx; Go teams can use stdlib http.Client with a transport wrapper and go‑otel.


4) Fees that won’t page you at 3 a.m.

EVM (EIP‑1559):

  • Always set both maxPriorityFeePerGas and maxFeePerGas. Derive from recent fee history and clamp to budget; never rely on “auto” values from a provider. Return effectiveGasPrice in receipts to understand actual spend. (eips.ethereum.org)
  • Replace‑by‑fee safely: to bump a stuck tx, resubmit the same nonce with higher maxFeePerGas (and typically higher maxPriorityFeePerGas). Track in your store by nonce, not by hash.

EVM blobs (EIP‑4844):

  • Include maxFeePerBlobGas and blobVersionedHashes in your transaction payload, and note that to must be a non‑nil address (blob txs aren’t contract creations). If you archive or index, be aware blobs are pruned (~2 weeks); plan external DA if needed. (eips.ethereum.org)

Solana (priority fees):

  • Total prioritization fee = CU limit × microLamports per CU. Use ComputeBudgetProgram.setComputeUnitLimit and setComputeUnitPrice, and size based on simulation + headroom. Feed pricing from getRecentPrioritizationFees and respect commitment for reads. (solana.com)

5) Real‑time without chaos: WebSockets done right

  • Use separate WS URLs/keys; they’re distinct from HTTP endpoints on most providers.
  • Implement: heartbeat pings, exponential backoff with jitter, resubscribe on reconnect, and “gap fill” via eth_getLogs/newHeads since the last seen block.
  • Prefer logs subscriptions for app‑level events; newHeads for pipeline triggers or indexing streams. eth_subscribe is WebSocket‑only; treat HTTP as a fallback with polling. (alchemy.com)

6) Wallet connectivity: modern discovery and sessioning

EIP‑6963 (multi‑wallet discovery):

  • Browser dapps should no longer assume a single window.ethereum. Use EIP‑6963’s event‑based discovery to list multiple injected providers deterministically and let users pick. This removes the “last extension wins” race. (eips.ethereum.org)

Example (simplified):

type ProviderDetail = { info: { uuid:string; name:string; icon:string; rdns:string }, provider: any }
const wallets: ProviderDetail[] = []
window.addEventListener('eip6963:announceProvider', (event: any) => {
  wallets.push(event.detail)
})
window.dispatchEvent(new CustomEvent('eip6963:requestProvider'))

// Now render a modal from `wallets` and connect to the chosen EIP-1193 provider

WalletConnect v2 (namespaces):

  • Session proposals must specify requiredNamespaces (methods, chains, events). For multi‑chain apps, request eip155 plus others (e.g., solana) explicitly; wallets respond with session namespaces listing concrete accounts (CAIP‑10). If proposal is invalid, wallets reject with code 1006. Bake this into your session UX. (specs.walletconnect.com)

Example proposal snippet:

{
  "requiredNamespaces": {
    "eip155": {
      "methods":["eth_sendTransaction","eth_signTypedData","personal_sign"],
      "chains":["eip155:1","eip155:8453"],
      "events":["accountsChanged","chainChanged"]
    },
    "solana": {
      "methods":["solana_signTransaction","solana_signMessage"],
      "chains":["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
      "events":[]
    }
  }
}

7) Account Abstraction (ERC‑4337) without surprises

If you’re planning “gasless” or passkey wallets, your API surface expands beyond eth_sendRawTransaction:

  • Users sign and send UserOperation objects to a bundler via eth_sendUserOperation; bundler simulates (simulateValidation) and submits handleOps on the EntryPoint contract. Paymasters can sponsor gas or accept ERC‑20 for fees. (eips.ethereum.org)

Practical guidance:

  • Lock your EntryPoint version per network (v0.6 vs v0.7+) and keep addresses in config. Provider ecosystems (e.g., Infura/MetaMask) expose bundler JSON‑RPC; don’t roll your own first. (docs.metamask.io)
  • If you use a verifying paymaster, treat its signer like production‑grade infra; Coinbase’s reference contracts are a good starting point to read. (github.com)
  • Track alt‑mempool outcomes with metrics: simulation fail rates by class (signature, paymaster, initCode), inclusion latency, and refund breakdowns. Your SDK can wrap bundler calls exactly like eth_ methods and reuse the same OTel spans. (docs.erc4337.io)

8) Networks and testnets (2025‑2026 planning)

  • For app/tooling development, Sepolia remains the default testnet; Hoodi is for validators/staking and protocol testing. Holešky is end‑of‑life in September 2025; plan migrations and don’t onboard new work there. A Sepolia replacement is planned for March 2026. Keep these timelines in your release plans and developer docs. (ethereum.org)

9) Production patterns that prevent fire drills

Reliability and rate limits

  • Horizontal provider failover: keep at least two RPCs configured per chain; rotate per request and degrade gracefully (read methods first, writes stricter).
  • Backoff with jitter and circuit breaking around 429/5xx; surface provider status in dashboards.
  • Batch read fanouts instead of 50 sequential calls. (geth.ethereum.org)

Idempotency and nonce discipline (EVM)

  • Your source of truth for write requests is (sender, nonce). If you send the same nonce twice, the network dedupes. Keep a per‑sender nonce lock and a replacement policy (higher fees only) to avoid accidental doubles.

Websocket robustness

  • Implement ping/pong, jittered reconnect, resubscribe, and block‑range reconciliation on resume (logs + fromBlock). Use WS for events; use HTTP batching for backfills.

Observability you can actually use

  • Emit OTel spans per JSON‑RPC method with rpc.system=jsonrpc, rpc.method, server.address, request_id, and rpc.jsonrpc.error_code/error_message on failures. Wire histograms for rpc.client.duration by method and endpoint; this makes “provider vs app” debates evidence‑based. (opentelemetry.io)

Security and key management

  • Treat API keys like credentials: restrict by IP/Origins, rotate, and never bundle in web apps.
  • For signing, centralize HSM/secure enclaves (custodial) or EIP‑4337 smart accounts (non‑custodial) with well‑defined scopes.
  • On Solana, hot keys are common; minimize their blast radius and use fee‑payer vaults for high‑volume flows.

10) A thin internal SDK that ages well

Resist writing a monolith. Define a small contract your app teams can rely on:

  • Typed method wrappers with narrow inputs/outputs (e.g., getErc20Balance, sendEip1559Tx, subscribeLogs)
  • Transport layer with retries, metrics, and feature flags (toggle providers, WS on/off)
  • Fee policy and chain config modules (EVM vs Solana)
  • OpenRPC schemas shipped alongside your SDK for docs and codegen; consider exposing rpc.discover internally to help downstream teams validate and test. (spec.open-rpc.org)

Example OpenRPC doc fragment for your internal “evm-ops” service:

{
  "openrpc": "1.3.2",
  "info": { "title": "EVM Ops RPC", "version": "0.1.0" },
  "methods": [
    {
      "name": "evm.sendRawTransaction",
      "params": [{ "name": "raw", "schema": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$" } }],
      "result": { "name": "txHash", "schema": { "type": "string" } }
    },
    {
      "name": "evm.subscribeLogs",
      "params": [{ "name": "filter", "schema": { "$ref": "#/components/schemas/LogFilter" } }],
      "result": { "name": "subscriptionId", "schema": { "type": "string" } }
    }
  ]
}

11) Chain‑specific gotchas we see in audits

  • EVM log filters: always constrain address OR topic0; unbounded filters produce timeouts and rate‑limit bans.
  • EIP‑4844: some providers throttle blob gossip differently; budget retries and longer timeouts for blob submissions and consider out‑of‑band DA mirrors if you must rehydrate data. (eips.ethereum.org)
  • Solana: ordering of ComputeBudget instructions matters; setComputeUnitLimit should precede the instructions that consume CUs. Watch commitment: user‑visible balances should be confirmed/finalized, not processed. (solana.com)

12) A short, end‑to‑end example: from cURL to SDK to AA

  1. Prototype a read and a write:
  • eth_call → confirm contract ABI is reachable
  • eth_feeHistory → pick fee envelope
  • eth_sendRawTransaction → signed type‑2 tx (track by nonce)
  1. Add real‑time:
  • eth_subscribe logs → your contract’s event topic
  • WS client with auto‑reconnect and backfill
  1. Upgrade wallets:
  • Add EIP‑6963 wallet discovery for browser flows
  • Add WalletConnect v2 with requiredNamespaces for EVM + Solana
  1. Add AA milestones:
  • Send a UserOperation to a managed bundler (Infura/MetaMask, Pimlico, etc.)
  • Integrate a verifying paymaster (sponsor gas for onboarding paths)
  • Emit OTel spans for eth_sendUserOperation and track simulation failure classes out of the box (docs.metamask.io)
  1. Ship to testnet with an exit plan:
  • Sepolia for app testing; Hoodi if you need validator/protocol testing; avoid new work on Holešky (sunset 2025‑09). Document these defaults for your teams. (ethereum.org)

13) “Copy‑paste‑able” snippets your team can reuse

  • Ethers v6: send a type‑2 tx with fee caps
import { ethers } from 'ethers'
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!)
const wallet = new ethers.Wallet(process.env.PRIVKEY!, provider)

const fee = await provider.send('eth_feeHistory', [5, 'latest', [50]])
const base = BigInt(fee.baseFeePerGas.slice(-1)[0])
const maxPriorityFeePerGas = 2_000_000_000n
const maxFeePerGas = base * 2n + maxPriorityFeePerGas

const tx = await wallet.sendTransaction({
  to: '0x...', value: 0n, data: '0x',
  maxPriorityFeePerGas, maxFeePerGas
})
await tx.wait()
  • Solana web3.js: set priority fees correctly
import { Connection, Keypair, SystemProgram, Transaction, ComputeBudgetProgram } from '@solana/web3.js'
const conn = new Connection('https://api.mainnet-beta.solana.com', 'confirmed')
const payer = Keypair.fromSecretKey(/* ... */)
const cuPrice = 15000 // microLamports per CU, from getRecentPrioritizationFees
const cuLimit = 300_000

const ixLimit = ComputeBudgetProgram.setComputeUnitLimit({ units: cuLimit })
const ixPrice = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: cuPrice })

const ix = SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: payer.publicKey, lamports: 1 })
const tx = new Transaction().add(ixLimit, ixPrice, ix)
const sig = await conn.sendTransaction(tx, [payer])
await conn.confirmTransaction(sig, 'confirmed')

Use getRecentPrioritizationFees to adapt cuPrice per workload and time of day. (solana.com)


14) Launch checklist (print this)

  • RPC
    • Two providers per chain configured; rotation + health checks
    • WebSocket reconnection + resubscription tested
    • Batching enabled for read fanouts
  • Fees
    • EIP‑1559 caps from fee history; blob fee caps when applicable
    • Solana CU limit/price set via ComputeBudget; pricing sourced from recent fees
  • Wallets
    • EIP‑6963 discovery; WalletConnect v2 namespaces validated
  • AA (if used)
    • Bundler endpoint SLAs; EntryPoint version pinned; paymaster keys hardened
  • Observability
    • OTel spans for every JSON‑RPC; rpc.method and error_code recorded
    • SLOs for rpc.client.duration p95 per method/endpoint
  • Testnets
    • Default: Sepolia for app dev; Hoodi for validator/protocol work; no new Holešky dependencies (ethereum.org)

Where 7Block Labs helps

We harden teams’ blockchain integrations: API baselines, SDK design/review, fee policies, AA rollouts, and production observability. If you want a time‑boxed “from cURL to SDK” engagement (2–4 weeks) with code you keep, we’d love to talk.


Sources and further reading


Like what you're reading? Let's build together.

Get a free 30‑minute consultation with our engineering team.

Related Posts

7BlockLabs

Full-stack blockchain product studio: DeFi, dApps, audits, integrations.

7Block Labs is a trading name of JAYANTH TECHNOLOGIES LIMITED.

Registered in England and Wales (Company No. 16589283).

Registered Office address: Office 13536, 182-184 High Street North, East Ham, London, E6 2JA.

© 2025 7BlockLabs. All rights reserved.