IoTeX RPC: A Developer's Guide (Chain ID 4689, DePIN)

By John Sullivan · October 26, 2026 · 11 min read · #iotex #iotex rpc #depin #evm #developer guide

IoTeX RPC (chain ID 4689) is the JSON-RPC surface of an EVM-compatible Layer 1 for DePIN. It looks like a normal EVM chain until you read a receipt. Our October 26, 2026 measurements found 2.60-second blocks, a flat 1,000 gwei base fee, and two production traps: a failed contract call returned status 0x6a, not 0x0, and eth_getTransactionReceipt returned an extra log that eth_getLogs did not. This guide covers the endpoint, fees, finality, logs, archive limits, tracing and the native actions that share each block.

The IoTeX RPC endpoint

https://rpc.swiftnodes.io/rpc/iotex?key=YOUR_API_KEY
wss://rpc.swiftnodes.io/ws/iotex?key=YOUR_API_KEY

The proxy slug is iotex. The chain ID is 4689 (0x1251), IOTX is the native gas token with 18 decimals, and the public explorer is iotexscan.io. The official full-node endpoint is https://babel-api.mainnet.iotex.io; the official archive endpoint is https://archive-mainnet.iotex.io. Both are listed in IoTeX's RPC endpoint documentation.

import { createPublicClient, defineChain, http } from "viem";

const iotex = defineChain({
  id: 4689,
  name: "IoTeX",
  nativeCurrency: { name: "IoTeX", symbol: "IOTX", decimals: 18 },
  rpcUrls: {
    default: { http: ["https://rpc.swiftnodes.io/rpc/iotex?key=YOUR_API_KEY"] },
  },
});

const client = createPublicClient({ chain: iotex, transport: http() });
const chainId = await client.getChainId(); // 4689
curl -sS https://rpc.swiftnodes.io/rpc/iotex?key=YOUR_API_KEY \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'
# {"jsonrpc":"2.0","id":1,"result":"0x1251"}

IoTeX's docs describe the EVM surface as Shanghai-compatible with Solidity 0.8.20. Compile for Shanghai if you use newer Solidity features; a Cancun-only opcode is not guaranteed. The docs also describe two address formats: native io1... addresses and Ethereum-style 0x addresses. The EVM JSON-RPC uses the 0x form; the address converter handles the other direction.

What we measured

All values below are from calls made on October 26, 2026 through the SwiftNodes endpoint and, where noted, the official babel-api.mainnet.iotex.io node. The block sample covers 100 consecutive blocks ending at 53,783,948.

Property Measured value
Chain ID 4689 (0x1251)
Client v2.5.1.2/go version go1.27.3 linux/amd64
Block interval 2.596 s average over 100 blocks; 2 s minimum, 5 s maximum
Transactions 108 in 100 blocks; 1.08 per block
Blocks with zero gas used 92 of 100
Block gas limit 10,000,000 on most EVM blocks; 0 on native-only blocks; not a chain constant (we also saw 21,000)
Base fee 1,000,000,000,000 wei (1,000 gwei) in every sampled block
eth_gasPrice 1,000,000,000,000 wei
eth_maxPriorityFeePerGas 1,000,000,000,000 wei
eth_feeHistory reward 0x0 at the 25th, 50th and 75th percentiles
Plain transfer 21,000 gas; 0.021 IOTX at the base fee
safe / finalized / pending all returned the latest block
eth_getLogs free tier 100 blocks per request
eth_getLogs upstream 2,001 blocks accepted; 2,002 rejected
State history latest 3,000 blocks on the public full node
WebSocket newHeads delivered in 429 ms
Multicall3 deployed at 0xcA11bde05977b3631167028862bE2a173976CA11

IoTeX is an EVM chain with native actions in the same blocks

IoTeX calls its transactions "actions," and the blockchain actions documentation lists Transfer, Execution and Governance actions. That distinction is visible in JSON-RPC. Blocks contain native actions alongside EVM transactions, and the native actions look like transactions with gas: "0x0", gasPrice: "0x0", and receipts with gasUsed: "0x0" and effectiveGasPrice: "0x0".

In our 100-block sample, 92 blocks had gasUsed: 0. They were not empty: each carried at least one native action, so eth_getBlockByNumber returned a transaction count above zero while the block gas fields were zero. The blocks with EVM execution carried their own gas limit: 10,000,000 was common, and one block reported 21,000. Native-only blocks reported 0.

This matters for indexing:

const block = await client.getBlock({ blockNumber, includeTransactions: true });
const evmTransactions = block.transactions.filter((tx) => tx.gas !== 0n);

const receipts = await client.request({
  method: "eth_getBlockReceipts",
  params: [block.number],
});
const evmReceipts = receipts.filter((r) => BigInt(r.gasUsed) > 0n);

Do not count every entry in transactions as an EVM call, and do not use block gasUsed as a measure of EVM activity. Filter by gas > 0 or by receipts with gasUsed > 0. For a DePIN data pipeline, this is the difference between counting device actions and counting contract calls.

Blocks are 2.6 seconds, and finality is immediate

IoTeX's Roll-DPoS documentation describes a round of roughly 2.5 seconds. The 100-block measurement came in at 2.596 seconds average. The minimum interval was 2 seconds and the maximum was 5 seconds. There were no repeated timestamps in the sample.

Roll-DPoS elects delegates, forms a 24-member committee with a VRF, and finalizes each proposed block in the same round with PBFT. That is why the safe, finalized and pending block tags all returned the same block as latest, on both the SwiftNodes route and the official node. Do not build a confirmation ladder around finalized on this chain; there is no multi-block finality lag to wait out. Confirm once and keep writes idempotent anyway. For the general block-tag picture, see how old the finalized tag is across 62 chains.

Fees: 1,000 gwei base, zero measured reward

Every block in the sample carried baseFeePerGas: 0xe8d4a51000, which is 1,000,000,000,000 wei or 1,000 gwei. eth_gasPrice returned the same value. eth_feeHistory returned the same base fee for the whole window, and the 25th, 50th and 75th percentile rewards were all 0x0.

A plain transfer estimates at 0x5208 (21,000 gas), so the base cost is:

21,000 × 1,000,000,000,000 wei = 21,000,000,000,000,000 wei
                                 = 0.021 IOTX

eth_maxPriorityFeePerGas returned 0xe8d4a51000 (1,000 gwei), the same as the base fee, while eth_feeHistory showed zero rewards. That is a contradiction worth handling explicitly. Read the live fee fields before sending, and do not assume the reported maximum priority fee is the amount you must add. For a plain transfer, the measured base fee is the number that matters. If you are sending contract calls, estimate gas and read the block's base fee at send time; do not hard-code 1,000 gwei forever.

A failed transaction can return status 0x6a

The most important receipt finding is that IoTeX does not limit status to 0x0 and 0x1.

We sampled transaction 0x2547…daa98 in block 53,783,834. The receipt from SwiftNodes and the official node both returned:

{
  "status": "0x6a",
  "gasUsed": "0x80626",
  "effectiveGasPrice": "0xe8d4a51000"
}

0x6a is 106. The receipt also carried one log entry, which the next section covers. The transaction called 0xa425E1197Ad096f926CE64C14e65aa4F89F756D0 with input 0x7f7524a6…; it used 525,862 gas. Replaying the same call with eth_call at that block returned execution reverted on both endpoints. The same replay at latest also reverted. The transaction failed, but the receipt did not use 0x0.

const receipt = await client.request({
  method: "eth_getTransactionReceipt",
  params: [hash],
});

if (receipt.status !== "0x1") {
  throw new Error(`IoTeX transaction failed: ${receipt.status}`);
}

Use === "0x1" for success and treat every other value as not successful. A check for status === "0x0" would classify this failed transaction as a success. A typed receipt helper that expects only 0x0 and 0x1 may also be a poor fit; read the raw field when the result matters.

Receipt logs are not the same as eth_getLogs

That block also exposed a second receipt problem: eth_getTransactionReceipt can include a log that eth_getLogs and eth_getBlockReceipts do not return.

Block 53,783,834 contained two transactions: the failed EVM call above and a native action. The failed transaction's receipt reported one log at 0xCCD3d8863D241BcC80f46302310a6d942A90e851 with logIndex: "0x1". eth_getLogs for the same block returned no logs for that transaction; it returned only the native action's log at logIndex: "0x0". eth_getBlockReceipts also returned zero logs for the failed transaction.

A later block, 53,783,872, showed the same pattern. The first transaction's receipt reported 16 logs while eth_getLogs and eth_getBlockReceipts returned 15. The second transaction's receipt reported 2 logs while the block sources returned 1. Each receipt carried one extra log at 0xCCD3…e851.

That extra log is visible to code that reads receipt.logs, but it does not appear in an eth_getLogs filter. We did not test a logs subscription. If your indexer uses both paths, reconcile them or choose one. For event indexing, use eth_getLogs:

const logs = await client.getLogs({
  address: contract,
  fromBlock,
  toBlock,
});

If you need per-transaction context, use eth_getTransactionReceipt for the status and gas, but treat its logs array as a superset. Do not assume a receipt log will be delivered by a logs subscription. For log decoding, see how event topics work and reading balances without mixing native and token units.

State is pruned after 3,000 blocks

The public full node is not an archive node. A state request for block 1 returned:

history is pruned, only supported for latest 3000 blocks, but requested height 1: not supported

The same error came back for heights 43,783,854 and 52,783,854. A height 100 blocks behind the head answered normally. Block bodies remain available: eth_getBlockByNumber returned block 1, but eth_getBalance and eth_getCode at that height failed.

The official docs list https://archive-mainnet.iotex.io as the archive endpoint. We tested it at block 1: eth_getBalance for 0x000000000000000000000000000000000000dEaD returned 0x0, and eth_getCode for 0x0000000000000000000000000000000000000001 returned 0x. If your application needs historical state, route those reads to an archive node or a provider that offers one. The default full node is for recent state, not for block 1 balances.

This is a common production break: a backfill starts at an old block, the block bodies load, and the first eth_getBalance or eth_getCode call fails. Fetch block bodies and receipts freely; gate historical state reads behind an archive check. See full node vs archive node for the general decision.

Logs: 100 blocks free, 2,001 upstream

The SwiftNodes free tier allows at most 100 blocks per eth_getLogs request. A 1,000-block request returned:

The free plan allows eth_getLogs over at most 100 blocks per request (this one spans 1000).
Split the range, or upgrade from Starter at $49/month.

The upstream node has its own cap. A 2,001-block range succeeded. A 2,002-block range returned:

{"code": 3, "message": "range exceeds the limit", "data": null}

A single-block query and a blockHash query both worked. For a backfill, chunk by 100 on the free tier and by 2,001 or less on paid plans:

const CHUNK = 100n; // free tier; paid plans can use up to 2,001
for (let from = start; from <= latest; from += CHUNK) {
  const to = from + CHUNK - 1n > latest ? latest : from + CHUNK - 1n;
  const logs = await client.getLogs({ address, fromBlock: from, toBlock: to });
}

Native actions and EVM calls share blocks, so a log query can return events from both. Filter by address and topic, and check the transactionHash and logIndex before joining to your own tables. For the general failure modes, see how eth_getLogs range caps bite.

Tracing, proofs and txpool: what is missing

Method support is uneven. On the official node, eth_getBlockReceipts returned an array of receipts. The other diagnostic methods did not:

  • debug_traceTransaction returned -32603 failed to collect era copy-on-write garbage: eracow: scan copied entries: erigon store does not support ordered range scan: not supported for the sampled transaction.
  • trace_transaction returned method: trace_transaction : web3 method not found.
  • eth_getProof returned method: eth_getProof : web3 method not found.
  • txpool_status returned method: txpool_status : web3 method not found.

The SwiftNodes free key also gates debug_* and trace_* behind a paid plan, but the official-node failures above are independent of that gate. Test the exact method on the exact node before you build a product feature around it. If you only need per-block receipts, eth_getBlockReceipts works; for traces, plan a fallback or a different provider.

WebSocket newHeads

A single eth_subscribe call with ["newHeads"] delivered a header in 429 ms:

wss://rpc.swiftnodes.io/ws/iotex?key=YOUR_API_KEY

Subscribe to newHeads when you need the next block without polling. The sample block intervals were 2 to 5 seconds, so a 1-second poll is not unreasonable either, but the subscription avoids the empty polls and gives you the block as it lands. Because native actions are included in blocks, filter the transactions inside the header before treating the block as EVM activity.

A production checklist

  • Use eth_getLogs for events. Reconcile any extra logs you see in eth_getTransactionReceipt.
  • Check status === "0x1" for success; treat 0x6a and every other value as not successful.
  • Filter gas > 0 or gasUsed > 0 before counting EVM calls; native actions share the block.
  • Read the base fee at send time. The measured base fee was 1,000 gwei, and a plain transfer cost 0.021 IOTX.
  • Do not add the reported eth_maxPriorityFeePerGas blindly; eth_feeHistory showed zero rewards in the same blocks.
  • Keep historical state reads on an archive node; the public full node only keeps the latest 3,000 blocks of state.
  • Chunk eth_getLogs at 100 blocks on the free tier and at most 2,001 blocks upstream.
  • Treat safe, finalized and pending as aliases of the latest block on this chain.
  • Test debug_*, trace_*, eth_getProof and txpool_* before relying on them; several are missing.
  • Use the 0x address format for EVM JSON-RPC; convert native io1 addresses when a device or wallet supplies one.

FAQ

What is the IoTeX chain ID?

4689 (0x1251). IOTX is the native gas token with 18 decimals.

What is the IoTeX RPC endpoint?

https://rpc.swiftnodes.io/rpc/iotex?key=YOUR_API_KEY for HTTP and wss://rpc.swiftnodes.io/ws/iotex?key=YOUR_API_KEY for WebSocket. The official full node is https://babel-api.mainnet.iotex.io.

Why did my transaction return status 0x6a?

We measured a failed call with status 0x6a; an eth_call replay returned execution reverted. Use status === "0x1" for success and everything else as failure.

Why do receipt logs differ from eth_getLogs?

A receipt can carry an extra log at 0xCCD3…e851 that eth_getLogs and eth_getBlockReceipts omit. Use eth_getLogs for event indexing and treat receipt logs as a superset.

Why does eth_getBalance fail at an old block?

The public full node keeps state for the latest 3,000 blocks. Use the official archive endpoint or an archive provider for older state.

What is the eth_getLogs limit?

100 blocks on the free tier. The upstream accepted 2,001 blocks and rejected 2,002.

Summary

  • IoTeX is an EVM-compatible DePIN L1 with chain ID 4689 and 18-decimal IOTX gas.
  • Blocks averaged 2.596 seconds over 100 blocks; safe, finalized and pending all returned the latest block.
  • The base fee was a flat 1,000 gwei in every sampled block; a plain transfer cost 0.021 IOTX.
  • A failed transaction returned status 0x6a, not 0x0; check for 0x1 on success.
  • Receipt logs can contain an extra 0xCCD3…e851 log that eth_getLogs and eth_getBlockReceipts do not return.
  • 92 of 100 sampled blocks had zero gas used because native actions share the chain; filter for EVM activity.
  • The public full node keeps only 3,000 blocks of state; use the archive endpoint for older balances and code.
  • eth_getLogs allows 100 blocks free and 2,001 upstream; debug tracing, eth_getProof and txpool_status are not generally available.

IoTeX endpoints, method support and archive notes are on our IoTeX RPC page. Start free with 250,000 requests per month; no card required.

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

  • Kava RPC: A Developer's Guide (Chain ID 2222)

    Kava runs an EVM beside a Cosmos SDK chain. Measured: 5.7 s blocks, instant finality, a 10,000-block getLogs cap, ~100 blocks of state, archive routing.

  • Aurora RPC: A Developer's Guide (EVM on NEAR)

    Aurora is an EVM running as a contract on NEAR. Measured: 0.6 s blocks, instant finality, a 2^53 gas limit, zero cumulativeGasUsed, NEAR receipt hashes.

  • Cronos RPC: A Developer's Guide

    How to use Cronos EVM over JSON-RPC — chain ID 25, ~0.42 second blocks with instant finality, a 10,000-block eth_getLogs cap, historical state pruned after ~100 blocks, and no debug or trace namespace.

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