Update balances, send notifications, or start jobs as new blocks and matching events arrive.
This guide subscribes to USDC Transfer logs over WebSocket, then shows how to fill the gap with
eth_getLogs when the connection drops, because a new subscription only delivers new events.
Prerequisites
- A GraphOps RPC API key.
- WebSocket access for the target chain.
- A WebSocket client. Use route-token authentication when the client cannot send custom headers.
- Durable progress tracking for the last processed block and log identity.
Subscribe to logs
This request subscribes to USDC Transfer logs over the Ethereum mainnet WebSocket endpoint. The
endpoint uses route-token authentication, so the URL contains your API key; redact it from logs,
metrics labels, and support output.
import WebSocket from "ws";
const socket = new WebSocket(
"wss://rpc.graphops.xyz/v1/evm/1/<GRAPHOPS_API_KEY>",
);
socket.on("open", () => {
socket.send(
JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "eth_subscribe",
params: [
"logs",
{
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
topics: [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
],
},
],
}),
);
});
socket.on("message", (message) => {
const payload = JSON.parse(message.toString());
console.log(payload);
});Response
The first message confirms the subscription and returns its id:
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x9cef478923ff08bf67fde6c64013158d"
}Each matching log then arrives as a notification:
{ "jsonrpc": "2.0", "method": "eth_subscription",A notification, not a reply "params": { "subscription": "0x9cef478923ff08bf67fde6c64013158d",The id eth_subscribe returned "result": {One log, shaped like an eth_getLogs entry "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"], "data": "0x00000000000000000000000000000000000000000000000000000001dcd65000", "blockNumber": "0x112a880",Store as your last processed block "transactionHash": "0xde0ca4c3db355459e8573d7d6f6373256ef1e883e331f3aaf9e4cb8bd668138d", "logIndex": "0x3d", "removed": falsetrue when a reorg reverses this log } }}Use a client library
Client libraries manage the subscription for you. Each example uses the route-token WebSocket endpoint; the curl tab shows the HTTPS backfill request you send after a disconnect.
curl cannot hold a subscription. Use it for the HTTPS backfill request that fills a gap:
curl "https://rpc.graphops.xyz/v1/evm/1" \
-H "Authorization: Bearer <GRAPHOPS_API_KEY>" \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getLogs",
"params": [
{
"fromBlock": "0x112a880",
"toBlock": "0x112a880",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
]
}
]
}'Use the route-token WebSocket endpoint with watchEvent:
import { createPublicClient, parseAbiItem, webSocket } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: webSocket("wss://rpc.graphops.xyz/v1/evm/1/<GRAPHOPS_API_KEY>"),
});
const unwatch = client.watchEvent({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
event: parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)",
),
onLogs: (logs) => console.log(logs),
});Use the route-token WebSocket endpoint with WebSocketProvider:
import { ethers } from "ethers";
const provider = new ethers.WebSocketProvider(
"wss://rpc.graphops.xyz/v1/evm/1/<GRAPHOPS_API_KEY>",
);
const filter = {
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
topics: [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
],
};
provider.on(filter, (log) => {
console.log(log);
});Use the route-token endpoint with the asynchronous WebSocket provider in web3.py 7:
import asyncio
from web3 import AsyncWeb3, WebSocketProvider
async def main():
async with AsyncWeb3(
WebSocketProvider("wss://rpc.graphops.xyz/v1/evm/1/<GRAPHOPS_API_KEY>")
) as w3:
await w3.eth.subscribe(
"logs",
{
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
],
},
)
async for payload in w3.socket.process_subscriptions():
print(payload)
asyncio.run(main())Supported subscriptions
| Subscription | Use it for |
|---|---|
newHeads | New block headers. |
logs | Logs matching an address and topic filter. |
newPendingTransactions | Pending transaction hashes when available for the network. |
syncing | Node sync status updates. |
Recover after a disconnect
- Store the last processed block and log identity as you handle each notification.
- Reconnect with exponential backoff after a disconnect.
- Resubscribe with the same filter.
- Backfill with
eth_getLogsfrom the block after the last one you processed up to the first block the new subscription delivers. - Deduplicate by
(blockHash, transactionHash, logIndex)where the backfill and the live stream overlap. - Treat logs with
removed: trueas reversals. See Retries and reorgs.