Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Integration & Development11 min read

Crypto Wallet API Architecture: Data, Signing, and RPC Layers

A practical guide to designing a crypto wallet API stack, separating indexed wallet data from signing and RPC access, with runnable verification steps.

TL;DR

A crypto wallet API is not one product. It is usually three distinct layers: an indexed data API that returns balances, tokens, and history for an address; a signing layer that keeps private keys or key shares under your control; and an RPC layer that reads chain state and broadcasts signed transactions. Treating these as one black box is the most common source of stale balances, failed broadcasts, and custody mistakes. This article gives you a mental model, runnable checks for each layer, a measurement procedure you can reproduce, and troubleshooting steps, then points to concrete endpoint selection where the choice is provider-specific.

What a crypto wallet API actually covers

The phrase crypto wallet api is used for at least three different interfaces. A wallet data API takes an address and returns structured JSON: native balances, token holdings, NFTs, transfers, and sometimes DeFi positions or profit-and-loss figures. A wallet-as-a-service API creates wallets, manages keys or key shares, and exposes send and receive operations. An RPC API exposes JSON-RPC methods such as eth_getBalance, eth_getTransactionCount, eth_sendRawTransaction, and eth_getTransactionReceipt directly against a node.

These layers fail in different ways. Indexed data can lag the chain head by seconds or blocks. Signing can fail because of nonce reuse, wrong chain ID, or a key-management policy. RPC can fail because of rate limits, reorgs, or a node that is behind. If you build a wallet feature on a single provider without knowing which layer you are calling, you cannot diagnose which of these is happening.

This article assumes you are building an application that displays wallet state and submits transactions, and that you want to keep key material under your own control. It does not assume a specific chain or provider. Where a concrete endpoint choice matters, use the OnFinality RPC Assistant rather than hard-coding a provider from a blog post.

  • Data layer: address in, portfolio and history out; usually indexed and cached.
  • Signing layer: unsigned transaction in, signed transaction out; keys never leave your boundary.
  • RPC layer: chain reads and transaction broadcast; closest to the node and the chain head.

Prerequisites and assumptions before you integrate

You need an RPC endpoint for each chain you support, an API key or credential for any indexed data provider, and a key-management decision: non-custodial keys held by the user, keys held by your backend in a KMS or HSM, or a wallet-as-a-service arrangement. Each choice changes your compliance surface and your recovery story.

You also need to decide your source of truth. For balances, the chain is authoritative but expensive to query at scale; an indexer is fast but eventually consistent. For transaction status, the receipt from the RPC node is authoritative, but you must handle reorgs. Write these decisions down before writing code, because they determine which API you call for each screen in your product.

Finally, confirm chain support explicitly. Do not assume that a data provider and an RPC provider support the same networks, testnets, or archive depth. Check the network list for the chains you plan to ship, and verify testnet availability separately from mainnet.

  • One RPC URL per chain, stored in a secret manager, never in client code.
  • A documented custody model and a key-rotation procedure.
  • A stated freshness target, for example balances no older than 30 seconds on the UI.
  • A reorg policy: how many confirmations before you mark a deposit final.

Mental model: indexer, signer, and node as separate services

Picture three services behind your wallet UI. The indexer subscribes to blocks, parses logs and traces, and writes address-keyed rows. The signer holds keys and produces signatures over serialized transactions. The node answers JSON-RPC calls and gossips transactions to peers.

A read path for a portfolio screen goes: UI → your backend → indexer. A write path goes: UI → your backend builds an unsigned transaction → signer signs → backend calls eth_sendRawTransaction on the node → backend polls eth_getTransactionReceipt. Keeping these paths separate means a lagging indexer never blocks a withdrawal, and a busy node never corrupts your displayed balances.

This separation also lets you swap providers per layer. You might use one vendor for indexed multichain data and a different RPC provider for low-latency broadcast. The OnFinality networks directory and dedicated node options are relevant when you need predictable RPC capacity rather than shared public endpoints.

Reading wallet state through an indexed data API

Most wallet data APIs expose a REST shape similar to GET /addresses/{address}/balances or a GraphQL query. The exact paths differ by vendor, so treat the following as a shape to adapt, not a literal contract. The important part is what you assert about the response: which chain, which block height, and how fresh the data is.

Always request or record a block height alongside balances. Without it you cannot tell whether a missing token is a real zero balance or an indexer that has not caught up. If the provider returns a synced_at or block_number field, store it with the response and surface staleness in your UI when it exceeds your threshold.

curl -s "https://WALLET_DATA_API_HOST/v1/addresses/0xYOUR_ADDRESS/balances?chain=ethereum" \
  -H "Authorization: Bearer $WALLET_DATA_API_KEY" \
  | jq '{block: .meta.block_number, synced_at: .meta.synced_at, tokens: [.balances[] | {symbol, amount, contract}]}'

Reading chain state directly over JSON-RPC

When you need the authoritative value at the chain head, call the node. The two calls below are standard Ethereum JSON-RPC and work on any EVM-compatible endpoint. Replace RPC_URL with your provider endpoint; never commit a real URL with an embedded key.

Compare the RPC balance with the indexed balance for the same address. A persistent gap usually means the indexer is behind, the address is on a different chain than you think, or the token is a contract balance that the native balance call does not include.

curl -s -X POST "$RPC_URL" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0xYOUR_ADDRESS","latest"]}' | jq '.result'

curl -s -X POST "$RPC_URL" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"eth_blockNumber","params":[]}' | jq '.result'

Building and signing a transaction without exposing keys

The safe pattern is: fetch nonce and gas parameters from the node, build the unsigned transaction in your backend, sign it inside your key boundary, then broadcast the raw bytes. The signer should never receive an RPC URL with write access, and the RPC credential should never be able to export keys.

The JavaScript example below uses ethers to build and sign locally. It reads the nonce from the node, signs with a key loaded from an environment variable, and prints the raw transaction. In production, replace the local wallet with a KMS or HSM signer and keep PRIVATE_KEY out of process memory wherever possible.

import { JsonRpcProvider, Wallet, parseEther } from "ethers";

const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY, provider);

const nonce = await provider.getTransactionCount(wallet.address, "pending");
const fee = await provider.getFeeData();

const tx = {
  to: "0xRECIPIENT_ADDRESS",
  value: parseEther("0.001"),
  nonce,
  chainId: (await provider.getNetwork()).chainId,
  maxFeePerGas: fee.maxFeePerGas,
  maxPriorityFeePerGas: fee.maxPriorityFeePerGas,
  gasLimit: 21000n,
};

const raw = await wallet.signTransaction(tx);
console.log(raw);
// Broadcast separately: await provider.broadcastTransaction(raw);

Verifying a broadcast and confirming finality

After broadcast, poll eth_getTransactionReceipt until it returns a receipt with a block number, then wait for your confirmation policy. A receipt with status: 0x1 means the transaction executed successfully; 0x0 means it reverted and gas was consumed. A null receipt means the node has not seen the transaction yet, which can mean propagation delay, a dropped transaction, or a nonce gap.

Record the transaction hash, the block number, and the timestamp at first receipt. That record is what you use later to reconcile against your indexer and to answer support questions. If the receipt never appears, check the nonce against eth_getTransactionCount with the pending tag before resubmitting.

curl -s -X POST "$RPC_URL" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"eth_getTransactionReceipt","params":["0xYOUR_TX_HASH"]}' \
  | jq '{status: .result.status, block: .result.blockNumber, gasUsed: .result.gasUsed}'

A reproducible measurement procedure for your wallet API stack

Do not trust vendor latency claims for your own workload. Measure the paths your users actually hit. Run each check at least 100 times over a fixed window, from the same region as your backend, and record p50, p95, and p99. Separate read latency (balance and history) from write latency (broadcast to first receipt).

Use a script that timestamps each request, then aggregate. The table below is the shape to fill in; the numbers must come from your own run, not from this article.

  • Read path: time GET /balances on the data API and eth_getBalance on the RPC endpoint for the same address.
  • Write path: time from eth_sendRawTransaction to the first non-null receipt.
  • Freshness: compare the indexer's reported block height with eth_blockNumber at the same moment.
  • Error rate: count HTTP 429, 5xx, and JSON-RPC error objects separately.
| Path | p50 (ms) | p95 (ms) | p99 (ms) | Error rate | Notes |
|------|----------|----------|----------|------------|-------|
| Indexed balances | | | | | record block height |
| RPC eth_getBalance | | | | | same address |
| Broadcast to receipt | | | | | same chain, same fee policy |
| Indexer lag (blocks) | | | | | head minus indexed |

Troubleshooting common wallet API failures

Balances look wrong or stale. Compare the indexer's block height with the RPC head. If the gap is growing, the indexer is behind; if the gap is zero but the balance differs, check whether you are reading a token contract balance versus native balance, and whether the address is checksummed correctly.

Broadcast returns nonce too low or replacement transaction underpriced. Another transaction with the same nonce was already mined or is pending. Query eth_getTransactionCount with pending, and if you intend to replace, raise the fee by the replacement threshold your node enforces.

Receipt never appears. The transaction may not have propagated. Re-broadcast the same raw bytes to a second endpoint, or check a block explorer. Do not sign a new transaction with the same nonce unless you intend to replace the first.

429 or rate-limit errors under load. Shared public endpoints throttle aggressively. Move read-heavy paths to an indexed data API and reserve RPC capacity for writes. For sustained throughput, a dedicated node removes noisy-neighbor effects.

Limitations and tradeoffs of each layer

Indexed data APIs are fast and convenient but eventually consistent, and their coverage of chains, tokens, and DeFi protocols varies. They also concentrate your dependency: an outage at the indexer blanks your portfolio screen even though the chain is healthy. Mitigate with a fallback RPC read for native balances and a cached last-known-good response.

RPC endpoints are authoritative but expensive to query for portfolio views, and archive queries for historical balances can be slow or unavailable on non-archive nodes. Signing locally gives you the strongest custody story but puts key-management, nonce management, and fee estimation on your team. Wallet-as-a-service shifts that burden but changes who holds keys and what you can prove to auditors.

There is no single correct split. A consumer app with many low-value reads should lean on indexed data. A treasury or exchange integration should treat the RPC receipt as the source of truth and use the indexer only for display.

Next steps for a production wallet integration

Start by writing down your custody model and your freshness target. Then stand up one chain end to end: indexed read, RPC read, local sign, broadcast, receipt. Measure that path with the table above before adding a second chain. Multichain support multiplies every failure mode, so prove the single-chain path first.

When you are ready to choose RPC endpoints, use the RPC Assistant for a concrete recommendation per network rather than copying an endpoint from an article. Review the OnFinality networks directory to confirm chain and testnet coverage, compare plans on the RPC pricing page, and consider a dedicated node if your write path needs predictable latency. For teams building broader Web3 infrastructure, the API service overview explains how managed RPC fits alongside your own services.

  • Document the read path and write path separately in your runbook.
  • Add a staleness indicator to any balance sourced from an indexer.
  • Alert on indexer lag and on receipt latency, not just HTTP errors.
  • Re-run the measurement procedure after any provider or plan change.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started