Arc RPC: A Developer's Guide (Chain ID 5042, USDC Gas)
Arc is Circle's stablecoin-native layer 1. The pitch is simple: pay gas in USDC, produce a block every half second, and treat a transaction as final when it lands. The JSON-RPC surface is ordinary EVM, so viem, ethers, Foundry and MetaMask connect without a custom client.
The differences show up in balances, logs and fees. Native USDC has 18 decimals while its ERC-20 interface has 6. One ERC-20 transfer emits two Transfer logs. The base fee has a 20 gwei floor, and block timestamps can repeat. Each is small on its own; together they break code that was written for Ethereum and never tested on Arc.
Everything below was measured on October 23, 2026, through the SwiftNodes endpoint and Arc's official RPC.
The endpoint
https://rpc.swiftnodes.io/rpc/arc?key=YOUR_API_KEY
wss://rpc.swiftnodes.io/ws/arc?key=YOUR_API_KEY
The proxy slug is arc. The chain ID is 5042 (0x13b2), the native gas token is USDC with 18 decimals, and the explorer is explorer.arc.io. If your library does not ship an Arc preset, define it with id 5042, currency USDC and the HTTP URL above.
What we measured
| Property | Value |
|---|---|
| Chain ID | 5042 (0x13b2) |
| Client | reth/v2.2.0-88505c7 and Geth/v10.0.0/drpc on the load-balanced route; official arc/v1 |
| First block | 2026-05-12 00:00:00 UTC |
| Average block interval | 0.5051 s over 100 blocks; 49 of 99 adjacent pairs shared a timestamp; no backwards timestamps |
| Transactions per block | 1.55 over 100 blocks (160 transactions; 27 empty blocks) |
| Block gas limit | 30,000,000 |
| Gas used in sample | 0.78% of the block gas limit |
| Base fee | 20 gwei in every block sampled |
eth_maxPriorityFeePerGas |
0.917 gwei |
eth_gasPrice |
20.917 gwei |
| Plain native transfer | 21,000 gas; about 0.00042 USDC at the 20 gwei base fee |
| Sampled ERC-20 transfer | 48,950 gas at 25.348 gwei; about 0.00124 USDC |
finalized tag |
matched the head in six snapshots; no lag |
eth_getLogs |
5,000-block safe range upstream; 100-block free-tier cap |
| Archive | code and balance answered 10,000,000 blocks back on the default route |
| WebSocket | newHeads delivered in 0.7 s |
The client line is not a typo. Requests through the load-balanced route landed on both reth/v2.2.0 and Geth/v10.0.0/drpc within a few minutes. The official endpoint reported arc/v1. That matters when a method works on one request and returns method not supported on the next: pin a backend, or use a provider that normalizes the method set.
USDC has two decimal scales
Arc does not have a wrapped USDC. The native asset is USDC, and the ERC-20 contract at 0x3600000000000000000000000000000000000000 is a second interface over the same balance. The native interface uses 18 decimals for gas accounting and msg.value; the ERC-20 interface uses 6 decimals for transfer, approve and balanceOf.
A sampled address showed both views of one balance:
eth_getBalance 25.215231352212423895 USDC (18 decimals)
USDC.balanceOf 25.215231 USDC (6 decimals)
The difference — 0.000000352212423895 USDC — is not lost. It is present onchain but cannot be represented by the 6-decimal ERC-20 view. A 0.0000004 USDC native transfer can therefore leave balanceOf unchanged. In the same 100-block sample, a plain native send moved 0.000004858165473895 USDC; depending on the truncated remainder, that moves the ERC-20 view by only four or five units. Do not use balanceOf diffs for native reconciliation.
That has two consequences:
- Use
eth_getBalancefor exact accounting, custody and reconciliation. Do not credit a native transfer from abalanceOfdiff. - Use the ERC-20 interface for token integrations that need
approve,transferFromor allowances, but remember it truncates below 0.000001 USDC.
const native = await provider.getBalance(address); // 18 decimals
const erc20 = await usdc.balanceOf(address); // 6 decimals
const exact = formatUnits(native, 18); // "25.215231352212423895"
const tokenView = formatUnits(erc20, 6); // "25.215231"
Do not treat native USDC and ERC-20 USDC as two assets. A pool that pairs msg.value (18 decimals) against balanceOf (6 decimals) without scaling is off by 10¹². Arc's docs recommend using the ERC-20 interface for application-level token integrations; for exact accounting, the native balance is the authoritative one. Do not mix the two raw values in one calculation.
One ERC-20 transfer, two Transfer logs
Arc ships EIP-7708 ahead of Ethereum. Every USDC send, ERC-20 transfer, mint and burn emits a standard Transfer log from the system emitter at 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE, with 18-decimal values. The emitter has no bytecode — eth_getCode returns 0x — but it is a real log source.
The ERC-20 contract also emits its own Transfer with 6-decimal values. So a single transfer() produces two logs with the same from, to and economic value:
| Log | Emitter | Decimals | Sample value |
|---|---|---|---|
| System (EIP-7708) | 0xffff…fffe |
18 | 0x0dd16ef70e363000 = 0.995699 USDC |
| ERC-20 | 0x3600…0000 |
6 | 0xf3173 = 995,699 units = 0.995699 USDC |
The system log is emitted first. In the sampled transaction, it was logIndex 0 and the ERC-20 log was logIndex 1. The transaction used 48,950 gas at 25.348 gwei, about 0.00124 USDC.
A plain native send emits only the system log. Our 21,000-gas sample moved 0.000004858165473895 USDC and produced one log from 0xffff…fffe.
For an indexer, choose one stream and key it by the emitter:
const NATIVE_USDC = "0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE";
const ERC20_USDC = "0x3600000000000000000000000000000000000000";
const TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
// All explicit USDC movements, one row per movement, 18 decimals.
const nativeLogs = await provider.getLogs({
address: NATIVE_USDC,
topics: [TRANSFER],
fromBlock,
toBlock,
});
// ERC-20-interface activity only, 6 decimals. Do not merge without a source key.
const erc20Logs = await provider.getLogs({
address: ERC20_USDC,
topics: [TRANSFER],
fromBlock,
toBlock,
});
If you only need payment reconciliation, index the system emitter and stop there. If you need approvals or transferFrom semantics, index the ERC-20 contract separately. Never count both rows as two transfers. The same rule applies to EIP-3009 relayed transfers: the from in the system log is the actual payer, not the relayer in tx.from.
In a 100-block sample there were 104 system Transfer logs and 64 ERC-20 Transfer logs. The system stream includes native sends, the native leg of ERC-20 transfers, contract endowments and mint/burn. Gas fees do not emit a Transfer; derive them from the receipt.
The fee floor is 20 gwei
Arc uses EIP-1559 pricing with a protocol floor. The base fee was exactly 20 gwei (0x4a817c800) in every block we sampled, and eth_gasPrice returned 20.917 gwei. The gap is the suggested priority fee: eth_maxPriorityFeePerGas returned 0.917 gwei.
The next block's base fee is published in the parent header's extra_data as an 8-byte big-endian value. The sampled header carried 0x00000004a817c800, which is 20 gwei. You can read it without another RPC call:
const nextBaseFee = BigInt("0x" + block.extraData.slice(-16));
The floor is the number that matters. Arc's docs say transactions with maxFeePerGas below 20 gwei may remain pending indefinitely or fail outright. Set maxFeePerGas to at least 20 gwei, then add a tip from eth_maxPriorityFeePerGas or eth_gasPrice. A zero tip can be accepted when the chain is quiet, but it is not a good default.
At the sampled prices:
| Transfer | Gas | Effective price | Cost |
|---|---|---|---|
| Plain native, base fee only | 21,000 | 20.000 gwei | 0.000420 USDC |
| Plain native, suggested price | 21,000 | 20.917 gwei | 0.000439 USDC |
| Sampled ERC-20 transfer | 48,950 | 25.348 gwei | 0.001241 USDC |
Those are dollar amounts because the gas token is USDC. A 0.00000486 USDC send that pays 0.00054 USDC in gas is spending more than 100 times the value moved; batch small payments, subsidize them with a paymaster, or keep them off-chain until they clear a sensible threshold.
Gas fees are not emitted as Transfer logs. Compute them from the receipt as gasUsed × effectiveGasPrice; that is the only reliable fee record for reconciliation.
Fast blocks, repeated timestamps
Arc produced a block every 0.5051 seconds over the 100-block sample. That is roughly two blocks per second, or 171,000 blocks per day. The chain was already at block 27.4 million when we measured.
Timestamps come from the proposer's wall clock at one-second granularity, so sub-second blocks can share a timestamp. In the sample, 49 of 99 adjacent pairs had the same timestamp. None went backwards. block.timestamp is non-decreasing, not strictly increasing.
Use the block number as the ordering key. Do not paginate by timestamp, use timestamp as a unique key, or assume a later block has a larger timestamp. For logs, (blockNumber, logIndex) is unique.
The chain is busy but not congested. The 100-block sample contained 160 transactions, or 1.55 per block; 27 blocks were empty. The transactions used 0.78% of the available gas. A one-second polling loop will usually fetch two blocks, but a WebSocket newHeads subscription is the better live feed.
Finality is the easy part. In six snapshots, finalized and safe returned the head block; neither tag lagged behind. That matches Arc's BFT design: a committed block is final, with no confirmation window and no reorg handling. A transaction is either unconfirmed or final. Still write idempotently — a retry can submit the same logical payment twice — but do not build a 12-block confirmation delay for Arc.
Indexing: 5,000 blocks upstream, 100 on the free tier
Arc's logs are dense enough that range planning matters. A separate 100-block span produced 599 logs in total, including 104 system USDC Transfer logs and 64 ERC-20 Transfer logs. At two blocks per second, that is about 12 logs per second.
On SwiftNodes, the free tier caps eth_getLogs at 100 blocks per request:
The free plan allows eth_getLogs over at most 100 blocks per request
(this one spans 101).
Paid plans remove that span cap. The upstream route is less consistent. A 5,000-block query was accepted on every upstream backend we tried. A 10,000-block query for USDC transfers returned 5,182 logs on one backend, while a 10,000-block empty query on another returned requested range too large. The backend, not the chain, decides. Start at 5,000 blocks and halve the range when the node rejects it.
let step = 5_000n;
for (let from = start; from <= end; ) {
const to = from + step - 1n > end ? end : from + step - 1n;
try {
const logs = await provider.getLogs({ address, fromBlock: from, toBlock: to });
await process(logs);
from = to + 1n;
} catch {
step /= 2n; // the error names a smaller range, or is a range cap
}
}
On the free tier, start with step = 100n. If you need every log in a range, filter by emitter or address; an unfiltered 100-block query returned 599 logs in our sample, and a wider unfiltered range is the fastest way to hit a result cap. For the general pattern, see eth_getLogs range caps.
eth_getBlockReceipts works and returns the receipt list for a block. That is cheaper than one eth_getTransactionReceipt per transaction when you are backfilling.
Archive, tracing and simulation
Historical state is available deeper than the chain's age suggests. eth_getCode for USDC and eth_getBalance both answered at a block 10,000,000 behind the head, which is about 58 days of history. The default route handled it on our free key. The explicit archive switch is a paid-plan feature:
https://rpc.swiftnodes.io/rpc/arc?key=YOUR_API_KEY&archive=1
A free key receives a plan error for archive=1, even though the default route happened to answer the historical read. If an archive read must be deterministic, use a paid archive route or pin a full-history backend. The general distinction is in full node vs archive node.
The method set differs by backend:
| Method | SwiftNodes route | Official Arc RPC |
|---|---|---|
eth_getBlockReceipts |
works | works |
eth_getProof |
works | method not supported |
txpool_status |
works (0x0 pending/queued) |
method not supported |
eth_createAccessList |
works | method not supported |
eth_simulateV1 |
works | method not supported |
debug_traceTransaction |
paid plan; free key refused | method not supported |
trace_transaction, trace_block |
paid plan; free key refused | method not supported |
WebSocket newHeads |
works | no public socket in the docs; use a provider |
The eth_getProof call on the SwiftNodes route returned a proof object. The official endpoint returned method not supported: eth_getProof, so do not assume the official URL is a superset. If your indexer needs proofs, simulations or txpool_status, route those calls to a provider that serves them.
Tracing is the one row we could not fully verify. A free SwiftNodes key returns:
Trace and debug methods (debug_traceTransaction) need a paid plan
Paid plans expose the methods, but we did not run a tracer against a paid key. Test the exact tracer and backend you plan to use before relying on callTracer output.
WebSocket newHeads worked through SwiftNodes. The socket opened and delivered a header in 0.7 seconds. A logs subscription on the free key was refused with the same plan message; paid plans support it. For production, subscribe to newHeads, fetch the new block over HTTP, and backfill any missed blocks after a reconnect. The reconnection pattern is in WebSocket reconnection without losing events.
Use Arc Foundry for local tests
A stock Anvil simulates a standard EVM. It does not reproduce Arc's 18-decimal native USDC, EIP-7708 logs, 20 gwei floor or value-transfer rules. Arc's docs ship a fork: arc-forge, arc-cast and arc-anvil.
arc-anvil --network arc
Use it for fork tests and transaction simulation. The standard Solidity, viem and ethers code still works; only the protocol differences need Arc-aware tooling.
A short list of other differences from the EVM differences reference:
PREVRANDAOalways returns0. Do not use it for randomness.- Blob transactions (type 3) are rejected.
BLOBHASHreturns0. - A value-bearing transfer to
address(0)reverts; a zero-value transfer succeeds. - A blocklisted sender or recipient causes the transaction to revert, and the gas is still consumed.
- A successful
SELFDESTRUCTthat moves a balance emits a systemTransfer; a later value-bearing call to the destroyed account reverts. - Block withdrawals are always empty.
None of these change ABI encoding or contract calls, but they change the edge cases. Test them on Arc, not on a generic Anvil fork.
FAQ
What is the Arc chain ID?
5042 (0x13b2). The native gas token is USDC with 18 decimals. The ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6 decimals and shares the same balance.
What is the Arc RPC endpoint?
https://rpc.swiftnodes.io/rpc/arc?key=YOUR_API_KEY for HTTP and wss://rpc.swiftnodes.io/ws/arc?key=YOUR_API_KEY for WebSocket. The official endpoint is https://rpc.mainnet.arc.io.
Why is my Arc USDC balance different from balanceOf?
They are the same balance at different precisions. eth_getBalance returns 18 decimals; balanceOf returns 6 and truncates the last 12 decimal places. Use the native value for exact accounting.
Why does one Arc USDC transfer emit two Transfer logs?
The system emitter logs every explicit USDC movement in 18 decimals; the ERC-20 contract logs its own 6-decimal event. One transfer() produces both. Filter by emitter address so you do not count the movement twice.
What is the eth_getLogs limit on Arc?
The SwiftNodes free tier allows 100 blocks per request. Paid plans remove that span cap. Upstream, 5,000-block ranges were accepted consistently; 10,000 blocks was backend-dependent. Start at 5,000 and halve on error.
How much does an Arc transaction cost?
The base fee was 20 gwei in every block sampled. A plain native transfer used 21,000 gas, about 0.00042 USDC at the base fee. A sampled ERC-20 transfer used 48,950 gas at 25.348 gwei, about 0.00124 USDC. Read the live fee methods before sending.
Is Arc finality instant?
Yes. Arc uses BFT consensus and deterministic finality. In six snapshots the finalized tag matched the head and never lagged. Act after one confirmation, but keep writes idempotent.
Summary
- Arc is Circle's USDC-native L1 with chain ID 5042, 0.505-second blocks and a 30,000,000 gas limit.
- Native USDC uses 18 decimals; the ERC-20 interface uses 6. Use
eth_getBalancefor exact accounting andbalanceOffor token integrations. - One ERC-20 transfer emits two
Transferlogs. Filter the system emitter at0xffff…fffeto avoid double-counting; a plain native send emits only that system log. - The base fee is 20 gwei, and the next base fee is in the parent header's
extra_data. AmaxFeePerGasbelow 20 gwei may remain pending or fail. - Finality is immediate, but timestamps can repeat. Order by block number, and subscribe to
newHeadsinstead of polling. - Logs are dense: 599 logs in 100 blocks. The free tier caps
eth_getLogsat 100 blocks; 5,000 blocks is a safe upstream range. Archive reads worked 10 million blocks back, but pin&archive=1on a paid plan for deterministic routing.
Arc endpoints and method support are on our Arc RPC page. For the wider context, see how old the finalized tag is across 62 chains and how eth_getLogs range caps bite.
Start free if you want to test the endpoint with your own key; no card is required.
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
- 0G RPC: A Developer's Guide (Chain ID 16661)
0G RPC (chain ID 16661) measured: 0.84 s blocks, a 7 wei base fee under a 4 gwei tip floor, no-bytecode precompiles, a 20,000-result log cap and a pruned default route.
- Lisk RPC: A Developer's Guide (Chain ID 1135, Shutdown Oct 31)
Lisk RPC (chain ID 1135) measured: 2 s blocks, ETH gas, a 1,015 wei base fee, 100k-block log caps, and the October 31 shutdown. Withdrawal steps and migration checks.
- Etherlink RPC: A Developer's Guide (Chain ID 42793)
Etherlink is the EVM rollup on Tezos. Measured: sub-second to 1.5 s blocks, a fixed 1 gwei base fee, plain transfers that use 621,000 gas because the data fee is charged as gas, a 500-block eth_getLogs cap, and no safe tag.