DEV Community

SwiftNodes
SwiftNodes

Posted on Originally published at swiftnodes.io

Solana RPC: A Developer's Guide to Endpoints, Methods, and Production Setup

Solana's RPC interface is unlike anything in the EVM world. There's no eth_call, no gas, no EVM bytecode. Instead, you interact with programs through serialized instructions, parse accounts by their owner, and deal with a chain that produces blocks every 400 milliseconds. If you're coming from Ethereum, everything you know about RPC needs to be relearned — the concepts are similar, but the implementation is completely different.

This is the practical reference for connecting to Solana via JSON-RPC. The methods, the commitment levels, the account model, and the production concerns that actually matter when you're building against Solana mainnet.

The essentials

Solana mainnet is chain ID 101 (in the cluster namespace), with:

  • Block time: ~400 milliseconds
  • Fee token: SOL
  • Consensus: Proof-of-history + Tower BFT
  • Finality: ~13 seconds (confirmed), ~33 seconds (finalized)
  • State model: Accounts, not storage slots — everything is an account

Solana doesn't use EVM-compatible JSON-RPC. The interface is its own specification, and the methods are named differently (getAccountInfo instead of eth_getBalance, sendTransaction instead of eth_sendRawTransaction). Any Solana SDK — @solana/web3.js, anchor, solana-py — wraps these methods.

Connecting: the setup

curl (the universal fallback)

# Get the latest blockhash
curl -s -X POST https://rpc.swiftnodes.io/rpc/sol?key=YOUR_API_KEY \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"getLatestBlockhash"}'

# Get an account's balance (in lamports)
curl -s -X POST https://rpc.swiftnodes.io/rpc/sol?key=YOUR_API_KEY \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"getBalance","params":["YOUR_PUBLIC_KEY"]}'
# -> {"jsonrpc":"2.0","result":{"context":{"slot":312000000},"value":1000000000}}

# Get the current slot
curl -s -X POST https://rpc.swiftnodes.io/rpc/sol?key=YOUR_API_KEY \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"getSlot"}'
Enter fullscreen mode Exit fullscreen mode

@solana/web3.js (the standard SDK)

import { Connection, PublicKey } from '@solana/web3.js';

const connection = new Connection(
  'https://rpc.swiftnodes.io/rpc/sol?key=YOUR_API_KEY',
  'confirmed'
);

// Get balance
const balance = await connection.getBalance(new PublicKey('YOUR_PUBLIC_KEY'));
console.log(`Balance: ${balance / 1e9} SOL`);

// Get latest blockhash
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();

// Get account info
const accountInfo = await connection.getAccountInfo(new PublicKey('PROGRAM_ID'));
console.log(`Owner: ${accountInfo.owner.toBase58()}`);
console.log(`Data length: ${accountInfo.data.length} bytes`);
Enter fullscreen mode Exit fullscreen mode

Anchor (for program interaction)

import { Program, AnchorProvider, setProvider } from '@coral-xyz/anchor';
import { Connection, Keypair } from '@solana/web3.js';

const connection = new Connection(
  'https://rpc.swiftnodes.io/rpc/sol?key=YOUR_API_KEY',
  'confirmed'
);
const provider = new AnchorProvider(connection, wallet, { commitment: 'confirmed' });
setProvider(provider);

const program = Program.atlas(PROGRAM_ID, idl);
const account = await program.account.myAccount.fetch(accountAddress);
Enter fullscreen mode Exit fullscreen mode

The methods you'll actually use

Solana has dozens of RPC methods. Here are the ones that cover 95% of real-world usage:

Method What it does When you need it
getLatestBlockhash Recent blockhash + valid-until height Every transaction
getBalance SOL balance for a public key Wallet UIs, balance checks
getAccountInfo Full account data + owner + lamports Reading any on-chain state
sendTransaction Submit a signed transaction Sending transactions
getSignatureStatuses Transaction status by signature Confirming transactions
getSignaturesForAddress Transaction history for an address Activity feeds, history
getProgramAccounts All accounts owned by a program Indexing, analytics
getBlock Full block with transactions Block explorers, analytics
getSlot Current slot number Health checks, sync monitoring
getHealth Node health status Monitoring, load balancers

The two methods that cause the most trouble

getProgramAccounts is the most expensive call on Solana. It returns every account owned by a program — for large programs like token mints or DEXes, that can be hundreds of thousands of accounts. The response can be hundreds of megabytes. Most RPC providers rate-limit or outright block this method.

The fix is to use dataSlice to request only the bytes you need, and filters to narrow the results:

const accounts = await connection.getProgramAccounts(
  new PublicKey('TOKEN_PROGRAM_ID'),
  {
    dataSlice: { offset: 0, length: 0 }, // Don't fetch account data
    filters: [
      { dataSize: 165 }, // Only token accounts (165 bytes)
      { memcmp: { offset: 0, bytes: mintAddress.toBase58() } }, // For a specific mint
    ],
  }
);
Enter fullscreen mode Exit fullscreen mode

Without filters, getProgramAccounts on a popular program will time out, get rejected, or eat through your rate limit in a single call.

sendTransaction has a subtlety that catches everyone: Solana transactions expire. Every transaction includes a recent blockhash, and if that blockhash is older than ~150 blocks (~1 minute), the transaction is rejected. If your transaction isn't landing, it's usually because the blockhash expired before the network included it.

The fix: use a durable nonce for transactions that need to survive longer than a minute, or implement a retry loop with fresh blockhashes:

async function sendWithRetry(connection, transaction, retries = 5) {
  for (let i = 0; i < retries; i++) {
    const { blockhash } = await connection.getLatestBlockhash('confirmed');
    transaction.recentBlockhash = blockhash;
    try {
      const sig = await connection.sendRawTransaction(transaction.serialize());
      await connection.confirmTransaction(sig, 'confirmed');
      return sig;
    } catch (err) {
      if (i === retries - 1) throw err;
      await new Promise(r => setTimeout(r, 1000));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Commitment levels

Solana's commitment model is different from Ethereum's block tags. Instead of latest/finalized, Solana uses a voting-based system:

Commitment Meaning When to use
processed The node has received the block but it hasn't been confirmed by the cluster Real-time UIs, non-critical reads
confirmed The block has been voted on by a supermajority of the validator set Default for most operations — good balance of speed and safety
finalized The block has been finalized by the cluster and cannot be reverted When reorgs are unacceptable (settlement, accounting)

The practical difference:

  • processed → ~400ms latency, but the block could be dropped
  • confirmed → ~5-10 seconds latency, very unlikely to be reverted
  • finalized → ~33 seconds latency, guaranteed permanent

For most applications, confirmed is the right default. Use finalized for anything involving value transfers or state that other systems depend on. Never use processed for anything that handles money.

// Different commitment for different operations
const balance = await connection.getBalance(pubkey, 'confirmed');
const tx = await connection.sendTransaction(signedTx);
await connection.confirmTransaction(tx, 'finalized'); // Wait for finality
Enter fullscreen mode Exit fullscreen mode

WebSocket subscriptions

Solana's WebSocket API is essential for real-time applications. Instead of polling getSlot or getSignatureStatuses, you subscribe to updates and the node pushes data to you.

import { Connection, PublicKey } from '@solana/web3.js';

const connection = new Connection(
  'wss://rpc.swiftnodes.io/ws/sol?key=YOUR_API_KEY',
  'confirmed'
);

// Subscribe to account changes
const subscriptionId = connection.onAccountChange(
  new PublicKey('ACCOUNT_ADDRESS'),
  (accountInfo, context) => {
    console.log(`Account changed at slot ${context.slot}`);
    console.log(`New data length: ${accountInfo.data.length}`);
  }
);

// Subscribe to new blocks
const slotSub = connection.onSlotChange((slotInfo) => {
  console.log(`New slot: ${slotInfo.slot}`);
});

// Subscribe to transaction logs for an address
const logSub = connection.onLogs(
  new PublicKey('PROGRAM_ID'),
  (logs, context) => {
    console.log(`Transaction at slot ${context.slot}:`);
    logs.logs.forEach(log => console.log(`  ${log}`));
  }
);

// Unsubscribe when done
connection.removeAccountChangeListener(subscriptionId);
connection.removeSlotChangeListener(slotSub);
connection.removeOnLogsListener(logSub);
Enter fullscreen mode Exit fullscreen mode

The common mistake is polling getSignatureStatuses in a tight loop to check if a transaction landed. On Solana's 400ms block time, this generates enormous request volume. Use onSignature over WebSocket instead:

const sig = await connection.sendRawTransaction(tx.serialize());
await new Promise((resolve, reject) => {
  connection.onSignature(sig, (result) => {
    if (result.err) reject(result.err);
    else resolve(result);
  }, 'confirmed');
});
Enter fullscreen mode Exit fullscreen mode

Parsing accounts

Everything on Solana is an account — tokens, NFTs, program state, even SOL balances are stored in system accounts. But accounts are just raw bytes. To make sense of them, you need to know the account's owner program and how that program serializes data.

Token accounts

Token accounts are owned by the Token Program (TokenkegQfeZyiNwAJbNbGKPFXCWuBv9DhWgFKTAhfBpJ6) and are 165 bytes. The layout is:

import { struct, u64, publicKey } from '@solana/buffer-layout-utils';

const TokenAccountLayout = struct([
  publicKey('mint'),
  publicKey('owner'),
  u64('amount'),
  // ... delegate, state, isNative, etc.
]);

const parsed = TokenAccountLayout.decode(accountInfo.data);
console.log(`Token balance: ${parsed.amount}`);
Enter fullscreen mode Exit fullscreen mode

Or use the built-in parser:

const tokenBalance = await connection.getTokenAccountBalance(tokenAccountPubkey);
console.log(`Balance: ${tokenBalance.value.uiAmount} ${tokenBalance.value.decimals} decimals`);
Enter fullscreen mode Exit fullscreen mode

Program-derived addresses (PDAs)

Many Solana programs store state in PDAs — accounts whose addresses are derived from a set of seeds and a program ID. To read PDA state, you need to know the seeds:

// Find the PDA for a user's game state
const [gameStatePDA, bump] = PublicKey.findProgramAddressSync(
  [Buffer.from('game_state'), userPubkey.toBuffer()],
  programId
);

const gameState = await program.account.gameState.fetch(gameStatePDA);
Enter fullscreen mode Exit fullscreen mode

Transaction simulation

Before sending a transaction, simulate it to catch errors without spending fees:

const simulation = await connection.simulateTransaction(signedTx);
if (simulation.value.err) {
  console.error('Transaction would fail:', simulation.value.err);
  console.error('Logs:', simulation.value.logs);
} else {
  console.log('Simulation passed, compute units:', simulation.value.unitsConsumed);
  // Now send the real transaction
  const sig = await connection.sendRawTransaction(signedTx.serialize());
}
Enter fullscreen mode Exit fullscreen mode

Simulation returns the same logs that the transaction would produce if executed, including compute unit consumption. Use this to set the compute unit limit precisely and avoid overpaying for fees.

Priority fees

Solana's base fee is 5,000 lamports per signature (~$0.00001). During congestion, you can add a priority fee to get your transaction included faster:

import { ComputeBudgetProgram } from '@solana/web3.js';

// Get the recent priority fees for a specific program
const fees = await connection.getRecentPrioritizationFees({
  lockedWritableAccounts: [new PublicKey('JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4')]
});

// Calculate a reasonable priority fee (median of recent fees)
const sortedFees = fees.map(f => f.prioritizationFee).sort((a, b) => a - b);
const medianFee = sortedFees[Math.floor(sortedFees.length / 2)];

// Add the priority fee instruction to your transaction
const priorityFeeIx = ComputeBudgetProgram.setComputeUnitPrice({
  microLamports: medianFee,
});

transaction.add(priorityFeeIx, ...otherInstructions);
Enter fullscreen mode Exit fullscreen mode

During normal conditions, no priority fee is needed. During high congestion (popular NFT mints, DeFi events), priority fees of 1,000-100,000 micro-lamports per compute unit can make the difference between your transaction landing or expiring.

Rate limits and production concerns

Solana RPC providers typically limit:

  • Requests per second — usually 10-100 RPS on free tiers
  • Heavy methodsgetProgramAccounts and getBlock may have separate, lower limits
  • Response size — some providers cap response payloads at 10-50MB
  • WebSocket connections — concurrent connection limits

The failure mode is HTTP 429 or a timeout on heavy methods. The fixes:

  1. Use filters and data slices on getProgramAccounts — this is non-negotiable in production
  2. Batch your requests — Solana supports JSON-RPC batching
  3. Cache account data — accounts don't change between slots, cache aggressively
  4. Use WebSocket subscriptions instead of polling — dramatically reduces request volume
// Batch multiple calls into one request
const batch = [
  { jsonrpc: '2.0', id: 1, method: 'getBalance', params: [pubkey1] },
  { jsonrpc: '2.0', id: 2, method: 'getBalance', params: [pubkey2] },
  { jsonrpc: '2.0', id: 3, method: 'getBalance', params: [pubkey3] },
];

const responses = await fetch(RPC_URL, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(batch),
});
Enter fullscreen mode Exit fullscreen mode

Choosing a Solana RPC endpoint

The decision comes down to three things:

1. Do you need getProgramAccounts? If your application indexes program state, you need a provider that supports this method with reasonable rate limits. Many free tiers block it entirely.

2. What's your transaction volume? Low-traffic dApps can use free tiers. Anything with real users will need a paid plan. Calculate your expected requests: a moderately active dApp with 1,000 daily users making ~20 reads each = 20,000 requests/day.

3. Do you need WebSocket subscriptions? If you're building anything real-time — a trading interface, a game, a monitoring dashboard — WebSocket support is essential. Not all providers offer it, and the ones that do may limit concurrent connections.

Other factors — latency, geographic distribution, stake-weighted quality of service — matter but are secondary. An endpoint with 50ms latency that blocks getProgramAccounts when you need it is useless.

The short version

Solana mainnet runs at ~400ms block time, SOL gas, Proof-of-history consensus. The RPC interface is its own spec — getAccountInfo, sendTransaction, getProgramAccounts, and the rest. Use confirmed for most reads, finalized when reorgs are unacceptable. Always filter getProgramAccounts with dataSlice and memcmp. Use WebSocket subscriptions instead of polling. Set compute unit limits from simulation results. Add priority fees during congestion.

For Solana RPC access across load-balanced validators with WebSocket support and no getProgramAccounts restrictions, grab a free API key and point your app at:

https://rpc.swiftnodes.io/rpc/sol?key=YOUR_API_KEY
Enter fullscreen mode Exit fullscreen mode

Top comments (0)