Ethereum Classic RPC: A Developer's Guide (Chain ID 61, No EIP-1559)

By John Sullivan · September 18, 2026 · 11 min read · #ethereum classic #rpc #developer guide #eip-1559 #proof of work

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_getBlockByNumber is served by CoreGeth/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_sendRawTransaction all 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_call to 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 logsBloom and 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 0x28fa6ae00the 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.

J
John Sullivan
Infrastructure Writer, SwiftNodes

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

Try SwiftNodes free — multi-chain RPC across 75+ networks, flat-rate pricing, pay by card or crypto, no KYC. Get an API key in 30 seconds →