Ethereum Classic RPC: A Developer's Guide (Chain ID 61, No EIP-1559)
Ethereum Classic is the awkward one in the EVM family. It isn't a rollup, isn't a sidechain, and didn't fork off Ethereum's design — it is the original Ethereum chain, continuing the history that the majority of the community abandoned at block 1,920,000 in July 2016. Every block before the fork is literally the same blocks, with the same hashes, on both chains.
For a developer, that shared lineage creates a specific set of assumptions that quietly stop being true. The fee market is the big one: Ethereum Classic never activated EIP-1559, so blocks carry no base fee, and the transaction-fee code you copied from an Ethereum tutorial is solving a problem this chain doesn't have. The other is finality: it's still proof-of-work, so there is no finalized block to wait for.
Everything below is measured through https://rpc.swiftnodes.io/rpc/etc on 2026-09-18, so you can re-run each probe yourself.
The essentials
- Chain ID: 61 (
0x3d) - Block height at time of writing: 25,367,599 (
0x183142f) - Block time: ~13–15 seconds (we measured 3 blocks across 45 seconds)
- Gas token: ETC, 18 decimals
- Consensus: Proof of work (Etchash)
- Fee market: Legacy
gasPrice. No EIP-1559 base fee - Reference client: Core Geth —
eth_getBlockByNumberis served byCoreGeth/hebeblock/v1.12.20-stable - Gas limit: 8,000,512 per block (
0x7a1200) - Archive history: Not available on our endpoints — see Archive access
The block limit is worth noticing: 8,000,512 gas per block, against Ethereum's 36 million. Your largest single transactions need to fit inside that, and a contract that pushes a block-size boundary on Ethereum may behave differently here.
Connecting: the setup
curl
# Chain ID — expect 0x3d (61), not 0x1
curl -s -X POST https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'
# -> {"jsonrpc":"2.0","id":1,"result":"0x3d"}
# Latest block
curl -s -X POST https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Legacy gas price — this is the fee number you actually use
curl -s -X POST https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":1}'
# -> {"jsonrpc":"2.0","id":1,"result":"0x28fa6ae00"} = 11 gwei
viem
Most tooling has an entry for Ethereum Classic, but the safest portable form is to state the chain explicitly — this works regardless of which version you're on, and it makes the one field that matters (chainId: 61) visible:
import { createPublicClient, http, defineChain } from 'viem';
const classic = defineChain({
id: 61,
name: 'Ethereum Classic',
nativeCurrency: { name: 'ETC', symbol: 'ETC', decimals: 18 },
rpcUrls: {
default: { http: ['https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY'] },
},
});
const client = createPublicClient({ chain: classic, transport: http() });
const blockNumber = await client.getBlockNumber();
const gasPrice = await client.getGasPrice(); // 11 gwei at time of writing
ethers v6
import { JsonRpcProvider } from 'ethers';
const provider = new JsonRpcProvider(
'https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY',
61, // chainId — pinning it stops the provider silently trusting a mismatched node
);
await provider.getBlockNumber();
Pinning the chainId in ethers is worth the one argument. Because ETC and ETH share a genesis and most of their history, a mislabelled endpoint is easy to mistake for a working one.
What's the same as Ethereum
The RPC surface is standard eth_*. Nothing exotic:
- Same methods.
eth_call,eth_getBalance,eth_getLogs,eth_getTransactionReceipt,eth_sendRawTransactionall behave as documented for Ethereum. - Same ABI and tooling. Solidity compiles and deploys unchanged. Foundry, Hardhat, viem, ethers all work.
- Same precompiles, including the bn256 pairings. This is the one I expected to be a problem and it isn't. An empty
eth_callto the pairing precompile returns the vacuously-true result on both chains:
# ecPairing (0x08) with empty input -> 0x...01 on both chains
curl -s -X POST https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x0000000000000000000000000000000000000008","input":"0x"},"latest"],"id":1}'
# -> {"jsonrpc":"2.0","id":1,"result":"0x0000...0001"}
Identical output from /rpc/eth and /rpc/base. Snark-verification libraries and anything leaning on ecadd/ecmul/ecpairing will not be the thing that breaks you here.
- Same receipt and log semantics, including
logsBloomand uncle handling.
What's different from Ethereum
There is no base fee
Ask for the latest block and count the fields:
curl -s -X POST https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":1}'
The returned object includes difficulty, mixHash, nonce, miner, totalDifficulty, sha3Uncles and uncles. It does not include baseFeePerGas. That absence is the whole story:
{ "number": "0x183142f", "gasLimit": "0x7a1200",
"difficulty": "0x6c54c9424b9f9", "miner": "0x…",
"mixHash": "0x…", "nonce": "0x…" }
// no baseFeePerGas key at all
The familiar maxFeePerGas = 2 × baseFee + tip recipe has nothing to multiply. Use eth_gasPrice and send a legacy transaction.
eth_maxPriorityFeePerGas answers — and that answer is a trap
This one deserves its own warning, because it will not fail loudly:
curl -s -X POST https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_maxPriorityFeePerGas","params":[],"id":1}'
# -> {"jsonrpc":"2.0","id":1,"result":"0x28fa6ae00"} = 11 gwei
It returns 0x28fa6ae00 — the exact same value as eth_gasPrice. The node is handing back its legacy gas-price suggestion through the 1559 method name. It is not a tip, and it is not a priority fee. Code that calls it, believes the name, and then sets maxFeePerGas to tip * 2 will produce a meaningless fee ceiling on this chain — and the estimation call won't stop you, because eth_estimateGas happily accepts EIP-1559-shaped transaction objects and returns a normal result.
Read the fee from eth_gasPrice. Treat the 1559 methods as absent even though one of them replies.
Finality is probabilistic
Proof of work means uncles, and uncles mean the head block can be superseded. There is no finalized block tag to wait on here, and no challenge window to reason about either — that's an optimistic-rollup concept. Your tool is confirmation count, and it needs to be larger than your reflex Ethereum number because blocks arrive roughly every 13–15 seconds rather than every 12.
For anything holding value, index and display the block you were included in and wait out a reorg, rather than assuming the latest block is durable. The mechanics are identical to any other PoW chain — our guide to handling reorgs in indexers covers the pattern.
Block numbers below 1,920,000 mean two different things
This is a data-modelling bug rather than an RPC bug, and ETC is the only chain where you'll hit it. Because the chains share history, block hashes below the fork are byte-identical. Ask both chains for block 1,919,999 — the last block before the fork:
for c in etc eth; do
curl -s -X POST https://rpc.swiftnodes.io/rpc/$c?key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x1d4bff",false],"id":1}' \
| python3 -c "import json,sys; print('$c', json.load(sys.stdin)['result']['hash'])"
done
Both return the same hash. Then ask for the next block:
| Block | Ethereum Classic | Ethereum | |
|---|---|---|---|
| 1,919,999 | 0xa218e2c611f21232d857e3c8… |
0xa218e2c611f21232d857e3c8… |
identical |
| 1,920,000 | 0x94365e3a8c0b35089c1d1195… |
0x4985f5ca3d2afbec36529aa9… |
diverged |
| 1,920,001 | 0xab7668dfd3bedcf9da505d69… |
0x87b2bc3f12e3ded808c6d4b9… |
diverged |
One block, and the two chains never speak to each other again. The genesis hash is likewise identical (0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3), as is every block up to the split.
So if you store transactions keyed on hash, or block heights without a chain discriminator, pre-fork records from Ethereum and Ethereum Classic will collide. A (chainId, blockNumber) or (chainId, txHash) composite key is the fix. This matters most if you run a multi-chain indexer, an airdrop/claims system, or anything with a transaction allowlist.
The methods you'll actually use
Nothing exotic. The list you'd use on Ethereum, with one substitution in the fee path:
| Task | Method | Note for ETC |
|---|---|---|
| Current height | eth_blockNumber |
— |
| Read a contract | eth_call |
— |
| Fee estimate | eth_gasPrice |
use this, not eth_maxPriorityFeePerGas |
| Gas units | eth_estimateGas |
works normally |
| Events | eth_getLogs |
keep ranges bounded |
| Receipt | eth_getTransactionReceipt |
check status, expect reorgs |
| Send | eth_sendRawTransaction |
legacy gasPrice transaction |
| Balance | eth_getBalance |
18 decimals |
For decoding events and reading balances, the mechanics are unchanged from Ethereum — the balance-reading traps and log decoding both apply exactly as written.
Archive access
Be deliberate about this if your project reads history, because Ethereum Classic is not an archive chain on our service and the failure mode differs by how you ask:
# state from an early block
curl -s -X POST "https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x0000000000000000000000000000000000000001","0x3e8"],"id":1}'
# -> {"jsonrpc":"2.0","id":1,"error":{"code":-32000,
# "message":"missing trie node 97fb274dbf… state … is not available, not found"}}
# explicitly asking for archive routing
curl -s -X POST "https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY&archive=1" ...
# -> {"error":"No archive node available for this chain","chain":"etc"}
Recent-state reads — the last stretch of blocks — work as expected. Deep historical state returns missing trie node, because the serving node is not keeping full historical state. Plain eth_getBlockByNumber for old blocks is fine; it's state-at-height (eth_getBalance, eth_getCode, eth_call at an old block) that isn't served.
If your workload is genuinely archive-dependent — backtesting balances, replaying old state, rebuilding historical dashboards — Ethereum Classic is a chain where you should confirm archive availability with a provider before committing. That's a real limitation, not a paperwork one: full node vs archive node explains what each tier costs and why providers draw the line where they do, and reading historical state covers what you can still do without it.
WebSocket subscriptions: verify before you build on them
The public status endpoint lists Ethereum Classic with wsAvailable: true and endpointTypes: ["http","ws"], and the socket does accept a connection:
wss://rpc.swiftnodes.io/ws/etc?key=YOUR_API_KEY
But when we tested it on 2026-09-18, an eth_subscribe to newHeads over that connection opened cleanly and then returned nothing — no subscription ID, no error — while the identical script against /ws/eth came back with a subscription ID immediately. A connection that opens is not the same thing as a subscription that streams.
So: treat WebSocket support on this chain as unverified on your provider, and design the HTTP path first. Polling for heads is genuinely fine at a 13–15 second block time:
let tip = await client.getBlockNumber();
setInterval(async () => {
const head = await client.getBlockNumber();
if (head > tip) { tip = head; /* process blocks tip-old..head */ }
}, 4000);
Track the block height you have actually processed rather than the head you last saw, and re-read on each new head. On a PoW chain a head you were told about can still be replaced, so a subscription — where it works — is a signal to re-read, not an append-only stream. Surviving WebSocket reconnects without losing events covers the resync pattern, and it applies equally to a polling loop that has to recover a gap.
Compared to the chains around it
| Ethereum | Ethereum Classic | Polygon zkEVM | Gnosis Chain | |
|---|---|---|---|---|
| Chain ID | 1 | 61 | 1101 | 100 |
| Consensus | PoS | PoW (Etchash) | Rollup + PoS | PoS (GPOS) |
| EIP-1559 fee market | Yes | No | Yes | Yes |
baseFeePerGas in blocks |
Yes | No | Yes | Yes |
| Block time | ~12s | ~13–15s | ~1s post-batches | ~5s |
| Deterministic finality | Yes | No — probabilistic | Proof-based | ~5 min |
| EVM | Yes | Yes | Yes | Yes |
The row that surprises people is fee market, not consensus: you can port a contract to ETC without touching it, but the signing code needs a look. The other surprise is that ETC is not an L2 — no sequencer, no forced-state inclusion, no withdrawal delay. L2 finality concepts don't apply; PoW confirmations do.
Production considerations
Confirmations, not finalized tags. Size your wait for a 13–15 second block time and probabilistic finality. Re-query on reorg.
Pin the chain ID. 61, everywhere it's accepted. Because pre-fork history is shared with Ethereum, a wrongly-configured endpoint can return plausible-looking data instead of an error.
Gas price is a node suggestion. eth_gasPrice returns the node's own policy (11 gwei at time of writing) rather than a market-clearing auction price. Watch your backlog instead of trusting one number — estimating gas properly explains where the Ethereum-style heuristics stop transferring.
eth_getLogs ranges. Bound them, the same as anywhere else — range caps and how they bite.
No archive for this chain. If your roadmap needs historical state on ETC, plan for it specifically rather than discovering it in production.
Errors are the same errors. If something fails, it will most likely look familiar — JSON-RPC error codes decoded is the reference.
Choosing an Ethereum Classic RPC endpoint
Three questions decide it:
1. Do you need historical state? If yes, verify archive support for ETC before you build on it. Many providers advertise archive broadly while a given chain's node is not actually keeping full state.
2. Do you need WebSocket? Don't take the checkbox on a provider's status page as a working subscription — open one and confirm it streams before you design around it.
3. What happens when it's slow? A single upstream makes latency and downtime your problem. Failover across independent endpoints is what keeps a reader alive — which is the point of routing through a layer that tries several upstreams per request and benches any that fall behind the chain head.
The short version
Ethereum Classic (chain ID 61) is a proof-of-work EVM chain sharing its entire history with Ethereum up to block 1,920,000 — so pre-fork block and transaction hashes are identical across both chains and need a chain discriminator in your schema. The RPC surface is standard eth_*, Solidity and viem/ethers work unchanged, and even the bn256 precompiles are present. What is genuinely different: no EIP-1559 (blocks have no baseFeePerGas, and eth_maxPriorityFeePerGas returns the legacy eth_gasPrice value — 11 gwei when measured — so use eth_gasPrice and send legacy transactions), probabilistic finality instead of a finalized block, ~13–15 second blocks under an 8,000,512 gas limit, and no archive state for this chain on our endpoints.
For Ethereum Classic RPC with per-request failover across independent upstreams, grab a free API key and point your app at:
https://rpc.swiftnodes.io/rpc/etc?key=YOUR_API_KEY
One key covers all 75 chains, and the free tier is 2 requests per second — enough to run every probe in this article while you verify the fork boundary for yourself.
John Sullivan covers RPC infrastructure, node operations, and multi-chain development at SwiftNodes — what it actually takes to keep endpoints fast, fresh, and reliable across EVM and non-EVM networks.
Related posts
- Arbitrum RPC: A Developer's Guide (Chain ID 42161, ~250ms Blocks, Fake Gas Limit)
How to connect to Arbitrum One over JSON-RPC — chain ID 42161, why the block gas limit is a sentinel value, what safe and finalized actually mean here, zero priority fees, and where archive reads stop working.
- Gnosis Chain RPC: A Developer's Guide to the Community-Owned EVM Chain
How to connect to Gnosis Chain via JSON-RPC — the dual-token model (xDAI + GNO), DAI-bridged stablecoin gas, consensus via GPOS, and what makes it different from other EVM chains. Includes production setup and fee optimization.
- Polygon zkEVM RPC: A Developer's Guide to the Type 1 ZK Rollup
How to connect to Polygon zkEVM via JSON-RPC — the Type 1 prover, equivalence testing, bridge architecture, and what makes it different from every other zkEVM. Includes production setup and fee optimization.