Solana

Solana Beta is live. Try BoltRPC Solana endpoints free - start your trial now.

How to Use viem with a Custom RPC Endpoint

Configure viem and wagmi to use a custom RPC endpoint. Covers createPublicClient, createWalletClient, transport setup, WebSocket support, and multi-chain configuration.

BoltRPC
BoltRPC Team
6 min read
How to Use viem with a Custom RPC Endpoint

How to Use viem with a Custom RPC Endpoint

viem is the modern TypeScript library for Ethereum. Faster, smaller, and more type-safe than web3.js or ethers.js, and the foundation that wagmi is built on. By default, viem uses public RPC endpoints that are slow and rate-limited. Swapping to a dedicated RPC endpoint takes less than five minutes.

This guide covers configuring viem and wagmi to use a custom RPC provider.


Install viem

npm install viem
# or
yarn add viem

Basic Setup: createPublicClient

createPublicClient is your read-only client for querying the chain: balances, blocks, logs, contract reads.

import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'

const client = createPublicClient({
  chain: mainnet,
  transport: http('https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'),
})

// Read the latest block
const block = await client.getBlockNumber()
console.log(`Latest block: ${block}`)

// Get ETH balance
const balance = await client.getBalance({
  address: '0xYourAddress',
})
console.log(`Balance: ${balance} wei`)

WebSocket Transport

For subscriptions (new blocks, pending transactions, contract events), use the WebSocket transport:

import { createPublicClient, webSocket } from 'viem'
import { mainnet } from 'viem/chains'

const wsClient = createPublicClient({
  chain: mainnet,
  transport: webSocket('wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY'),
})

// Subscribe to new blocks
const unwatch = wsClient.watchBlocks({
  onBlock: (block) => {
    console.log(`New block: ${block.number}`)
  },
})

// Subscribe to contract events
const unwatchLogs = wsClient.watchContractEvent({
  address: USDC_ADDRESS,
  abi: ERC20_ABI,
  eventName: 'Transfer',
  onLogs: (logs) => {
    console.log(`${logs.length} USDC transfers in latest block`)
  },
})

// Unsubscribe when done
// unwatch()
// unwatchLogs()

Fallback Transport

If one endpoint fails, fall through to the next automatically:

import { createPublicClient, http, fallback } from 'viem'
import { mainnet } from 'viem/chains'

const client = createPublicClient({
  chain: mainnet,
  transport: fallback([
    http('https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'),
    http('https://your-backup-rpc-url'),
  ]),
})

The fallback transport tries each endpoint in order and moves to the next on failure.


createWalletClient

For sending transactions, use createWalletClient alongside the public client:

import { createWalletClient, createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY')

const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http('https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'),
})

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http('https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'),
})

// Send ETH
const hash = await walletClient.sendTransaction({
  to: '0xRecipientAddress',
  value: 1000000000000000n, // 0.001 ETH in wei
})

// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log(`Confirmed in block ${receipt.blockNumber}`)

Read Contract Data

import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'

const ERC20_ABI = [
  {
    inputs: [{ name: 'account', type: 'address' }],
    name: 'balanceOf',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
] as const

const client = createPublicClient({
  chain: mainnet,
  transport: http('https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'),
})

const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'

const balance = await client.readContract({
  address: USDC_ADDRESS,
  abi: ERC20_ABI,
  functionName: 'balanceOf',
  args: ['0xYourAddress'],
})

console.log(`USDC balance: ${balance}`)

Multicall

Batch multiple contract reads into one RPC request using viem’s built-in multicall:

const [ethBalance, usdcBalance, daiBalance] = await client.multicall({
  contracts: [
    {
      address: WETH_ADDRESS,
      abi: ERC20_ABI,
      functionName: 'balanceOf',
      args: ['0xYourAddress'],
    },
    {
      address: USDC_ADDRESS,
      abi: ERC20_ABI,
      functionName: 'balanceOf',
      args: ['0xYourAddress'],
    },
    {
      address: DAI_ADDRESS,
      abi: ERC20_ABI,
      functionName: 'balanceOf',
      args: ['0xYourAddress'],
    },
  ],
})

// Three reads, one RPC call
console.log(ethBalance.result, usdcBalance.result, daiBalance.result)

viem’s multicall uses Multicall3 (deployed at 0xcA11bde05977b3631167028862bE2a173976CA11) automatically.


Multi-Chain Configuration

Configure multiple chains in a single file:

import { createPublicClient, http } from 'viem'
import { mainnet, arbitrum, base, polygon, optimism } from 'viem/chains'

const RPC_BASE = 'https://eu.endpoints.matrixed.link/rpc'
const AUTH = 'YOUR_KEY'

const clients = {
  ethereum: createPublicClient({
    chain: mainnet,
    transport: http(`${RPC_BASE}/ethereum?auth=${AUTH}`),
  }),
  arbitrum: createPublicClient({
    chain: arbitrum,
    transport: http(`${RPC_BASE}/arbitrum?auth=${AUTH}`),
  }),
  base: createPublicClient({
    chain: base,
    transport: http(`${RPC_BASE}/base?auth=${AUTH}`),
  }),
  polygon: createPublicClient({
    chain: polygon,
    transport: http(`${RPC_BASE}/polygon?auth=${AUTH}`),
  }),
  optimism: createPublicClient({
    chain: optimism,
    transport: http(`${RPC_BASE}/optimism?auth=${AUTH}`),
  }),
}

// Query any chain
const ethBlock = await clients.ethereum.getBlockNumber()
const arbBlock = await clients.arbitrum.getBlockNumber()

wagmi Configuration

If you are building a React application with wagmi, configure your custom RPC in the wagmi config:

import { createConfig, http, webSocket } from 'wagmi'
import { mainnet, arbitrum, base, polygon } from 'wagmi/chains'

const AUTH = 'YOUR_KEY'
const RPC_BASE = 'https://eu.endpoints.matrixed.link/rpc'
const WSS_BASE = 'wss://eu.endpoints.matrixed.link/ws'

export const wagmiConfig = createConfig({
  chains: [mainnet, arbitrum, base, polygon],
  transports: {
    [mainnet.id]: http(`${RPC_BASE}/ethereum?auth=${AUTH}`),
    [arbitrum.id]: http(`${RPC_BASE}/arbitrum?auth=${AUTH}`),
    [base.id]: http(`${RPC_BASE}/base?auth=${AUTH}`),
    [polygon.id]: http(`${RPC_BASE}/polygon?auth=${AUTH}`),
  },
})

With WebSocket for real-time subscriptions:

export const wagmiConfig = createConfig({
  chains: [mainnet, arbitrum],
  transports: {
    [mainnet.id]: webSocket(`${WSS_BASE}/ethereum?auth=${AUTH}`),
    [arbitrum.id]: webSocket(`${WSS_BASE}/arbitrum?auth=${AUTH}`),
  },
})

Then wrap your app:

import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { wagmiConfig } from './config'

const queryClient = new QueryClient()

export function App() {
  return (
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        {/* Your app */}
      </QueryClientProvider>
    </WagmiProvider>
  )
}

Environment Variables

Never hardcode your API key. Use environment variables:

// .env.local
NEXT_PUBLIC_BOLTRPC_KEY=your_api_key_here

// config.ts
const transport = http(
  `https://eu.endpoints.matrixed.link/rpc/ethereum?auth=${process.env.NEXT_PUBLIC_BOLTRPC_KEY}`
)

For server-side code (Node.js API routes, scripts):

// .env
BOLTRPC_KEY=your_api_key_here

// Never expose this key client-side
const transport = http(
  `https://eu.endpoints.matrixed.link/rpc/ethereum?auth=${process.env.BOLTRPC_KEY}`
)

FAQ

What is the difference between viem and wagmi?

viem is the low-level library that handles all blockchain interactions directly. wagmi is a React hooks library built on top of viem. It manages wallet connections, chain switching, and React state. If you are building a React app with wallet connect, use wagmi. If you are writing backend scripts or non-React code, use viem directly.

Does viem support WebSocket subscriptions?

Yes. Use the webSocket transport instead of http. WebSocket is required for watchBlocks, watchContractEvent, and watchPendingTransactions. The http transport does not support subscriptions.

Can I use viem with Next.js App Router?

Yes. For server components and API routes, use createPublicClient with http transport and your API key from process.env (not NEXT_PUBLIC_). For client components that need wallet interaction, use wagmi with the WagmiProvider in a client-side layout.

How does viem compare to ethers.js v6?

viem is tree-shakeable (smaller bundle), fully typed with TypeScript generics, and uses native BigInt throughout. ethers.js v6 also uses native BigInt but has a larger API surface. viem is generally preferred for new projects; ethers.js remains common in existing codebases and is often seen in Hardhat/Foundry test environments.

Why use a dedicated RPC instead of the default viem public endpoints?

viem’s default public endpoints are shared across all developers using the library. They have strict rate limits and no SLA. A dedicated endpoint gives you consistent performance, higher throughput for production workloads, and an auth key so your requests are never affected by other users’ traffic.


BoltRPC supports Ethereum, Arbitrum, Base, Polygon, Optimism and 16 other networks, all available via the same endpoint pattern, compatible with viem and wagmi out of the box.

Start your free 2-week trial: trial.boltrpc.io

Related: How to Connect Ethereum with ethers.js | Multi-Chain RPC Guide

Frequently asked questions

Ready to build with high-performance RPC?

Start your free trial today. No credit card required. Access 20+ networks instantly.

Disclaimer: The content in this article is for informational purposes only and does not constitute financial, legal, or technical advice. Code examples and configurations are provided as-is. Always verify information with official documentation and test thoroughly in your own environment before deploying to production.

Continue reading