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