zkSync Era (the official docs now write it ZKsync Era) is a ZK rollup on Ethereum that runs its own virtual machine, EraVM. It is not "Ethereum with a different RPC URL", it is not an OP Stack chain like Base or Optimism, and it does not behave exactly like the other zkEVMs in your network list. Accounts, transaction types, gas estimation, and contract deployment all have Era-specific rules. Wallet builders who paste chain ID 1, reuse the Sepolia testnet key, or carry Ethereum gas and address assumptions into Era ship stuck sends, wrong fee quotes, or funds bugs. Before you wire MetaMask, Rabby, Rainbow, or your own wallet_addEthereumChain flow, prove chain ID 324, learn the handful of EraVM differences that reach the wallet layer, and probe the URL you intend to ship.
This is not a ranking of providers. Use the zkSync Era Mainnet hub to compare endpoints and paid rows, then run the checks below on the URL you configure. If you only need the network in a browser wallet, the add zkSync Era to MetaMask page has the fields prefilled.
Chain ID 324 is not Ethereum and not zkSync Sepolia
Mainnet chain ID is 324, hex 0x144. The current testnet, ZKsync Era Sepolia, is 300 / 0x12c. Ethereum mainnet is 1. The old zkSync Era Goerli testnet (280) is deprecated; if it still appears in a config file, delete it rather than repurpose it. A wallet that already has Ethereum added still needs a separate zkSync Era network entry.
Wallet-add fields for mainnet, as published in the ZKsync docs:
- Network name:
ZKsync Era Mainnet(orzkSync Eraif the product already scopes the list) - Chain ID:
324(or0x144if the form wants hex) - Currency symbol:
ETH - RPC URL: a JSON-RPC HTTPS endpoint you have already probed
- Block explorer:
https://explorer.zksync.io
Native gas is ETH with 18 decimals, the same symbol as Ethereum L1 and most Ethereum L2s. Show the network name and chain ID in the UI, not the symbol alone, or users will not know which "ETH" balance they are looking at.
Call eth_chainId on the exact RPC URL you will ship. Expect 0x144. A gateway that returns 0x1 is Ethereum. A gateway that returns 0x12c is Sepolia. A timeout or an HTML error page is not zkSync Era mainnet for that transport. Run the check after any load balancer or fallback so you are testing the backend production actually hits. The official public RPC is https://mainnet.era.zksync.io (WebSocket wss://mainnet.era.zksync.io/ws), already listed on the hub; treat it as a probe target, not as proof that every branded gateway forwards to Era mainnet.
EraVM quirks that reach the wallet layer
zkSync Era is EVM-compatible, not EVM-equivalent. Contracts compiled for EraVM (with zksolc or zkvyper) run natively, and standard EVM bytecode runs through an EVM Bytecode Interpreter on top of EraVM. Most dapp calls look like Ethereum from the RPC side. The differences below are the ones a wallet or its backend actually hits:
- Native account abstraction. On Era, smart accounts and paymasters are protocol-level features, not an ERC-4337 add-on. The docs state that under the hood all accounts, even EOAs, behave like smart contract accounts and all accounts support paymasters, with one mempool for both. A wallet that only understands "EOA signs, EOA pays gas in ETH" will misread sponsored transactions and smart-account senders.
- EIP-712 transaction type
0x71. Legacy (0x0), EIP-2930 (0x1), and EIP-1559 (0x2) transactions work for plain sends. Paymaster flows, smart-account signatures (customSignature), and native contract deployment (factoryDeps) use type0x71(113), where the user signs an EIP-712 typed structure instead of the RLP payload. L1-originated priority transactions show up as0xff. Your transaction decoder, history view, and signing flow should not reject or mislabel these types. - No priority fee market. The docs recommend
maxPriorityFeePerGasof0and state that Era chains do not have a concept of priority fees. A tip slider copied from Ethereum does nothing useful here. - Pubdata is part of the gas bill. Era is a state-diff rollup, so pubdata cost is only known after execution and is post-charged per byte via
gas_per_pubdata. If the remaining gas cannot cover pubdata, the transaction reverts and the user still pays for computation. Storage writes are also precharged at the cold cost with a refund later. Always take the gas limit frometh_estimateGason an Era endpoint and add headroom; do not reuse a hard-coded 21000 or an estimate from Ethereum or an OP Stack L2. - Presigned Ethereum transactions may not replay. Gas values signed in an Era transaction are native EraVM gas, and the docs warn that presigned or keyless transactions created for Ethereum may be incompatible. Deterministic deployer tricks that depend on a keyless Ethereum transaction need an Era-specific path.
- Contract addresses are not portable. Native EraVM deployment goes through the
ContractDeployersystem contract, and Era splits nonces into a transaction nonce and a deployment nonce, sohash(RLP[address, nonce])does not predict an EOA-deployed address. The docs recommend reading theContractDeployedevent (or usingcreate2viaContractDeployer). Contracts deployed through the EVM interpreter follow Ethereumcreate/create2derivation. Either way, never hard-code an Ethereum token or router address into an Era wallet path. - Finality is batch- and proof-based. Blocks are grouped into L1 batches; a batch is committed to Ethereum, proven, and then executed after a time lock that governance has set to a 3 hour minimum. The docs put full finality at around 3 hours, sometimes longer.
zks_getTransactionDetailsreportsPending,Included,Verified, orFailed. Decide which oflatest,safe, orfinalized"confirmed" means, and keep withdrawal UX honest about the delay. zks_namespace. Era nodes expose methods such aszks_L1BatchNumber,zks_getTransactionDetails,zks_getL1BatchDetails,zks_gasPerPubdata, andzks_getL2ToL1LogProof. Some older ones (zks_estimateFee,zks_L1ChainId) are marked deprecated. Generic multichain gateways may only forwardeth_, so if your SDK or withdrawal flow callszks_, test it on the exact URL.- Not OP Stack. Era is not Base or Optimism. There are no Flashblocks, no OP Stack sequencer or derivation assumptions, and withdrawals are not governed by an optimistic fault-proof challenge window. Do not import a Base pending matrix or an Optimism withdrawal timer into an Era wallet path.
The ecosystem around Era is broader than one chain. ZKsync now calls its family of chains the Elastic Network, and the docs say new ZKsync chains use ZKsync OS, which is EVM-equivalent and does not support EraVM native account abstraction. Era itself still runs EraVM today. If you support several ZKsync chains, key each one by chain ID and do not assume Era behavior carries over.
Wallet-add gotchas that show up in QA
The failures that reach support are usually config, not cryptography:
- Wrong chain ID - Ethereum
1, Sepolia300, or the deprecated Goerli280pasted into a mainnet form. - Ethereum RPC on a zkSync Era row -
eth_chainIdwill not return0x144. - Explorer mismatch - sending users to
etherscan.iofor Era hashes, or tosepolia.explorer.zksync.iofor mainnet hashes. - One shared "zksync" RPC bucket - keep separate keys for
zksyncmainnet andzksync-sepoliaso a fallback cannot silently swap networks. Do the same for other Elastic Network chains, which have their own chain IDs. - Ethereum gas habits - fixed gas limits, a priority fee slider, or a fee quote copied from L1. Estimate on Era and budget for pubdata.
- Type
0x71blind spots - history or signing code that drops EIP-712 transactions, which breaks paymaster and smart-account flows. - Missing parent Ethereum - bridge and withdraw flows need chain ID
1configured even when the product lives on Era. - OP Stack assumptions - treating Era like Base or Optimism for pending tags, sequencer URLs, or withdrawal timing.
wallet_addEthereumChain vs wallet_switchEthereumChain
When you implement wallet_addEthereumChain, send a full EIP-3085 payload:
{
"chainId": "0x144",
"chainName": "ZKsync Era Mainnet",
"nativeCurrency": { "name": "Ether", "symbol": "ETH", "decimals": 18 },
"rpcUrls": ["https://your-probed-endpoint.example"],
"blockExplorerUrls": ["https://explorer.zksync.io"]
}
Wallets that receive only symbol in nativeCurrency can reject the request. The chain ID must be the hex string 0x144, not the number 324.
For wallet_switchEthereumChain, send only { "chainId": "0x144" }. Metadata fields belong on add, not switch. Prefer one primary RPC in the wallet payload; put failover in your own client, not as a list of public URLs the wallet may pick at random.
Public vs dedicated for wallet traffic
Public endpoints are fine for:
- Local development and wallet QA
- Low-volume
eth_chainId/eth_blockNumberprobes - One-off scripts and docs examples
They are a poor primary path when you:
- Run production wallet RPC for many users (balance, nonce, estimate, send, receipt polls)
- Depend on
zks_*methods for withdrawals, batch status, or fee data - Need predictable latency and a support channel when the gateway returns 429
- Depend on historical state, wide
eth_getLogs, or sustained WebSocket traffic - Cannot absorb shared-node brownouts during an airdrop, a mint, or a market spike
Probe the candidate URL before you call it ready:
eth_chainIdreturns0x144.- The head advances when you poll
eth_blockNumbertwice a few seconds apart. An unchanged balance is not proof the node is live. - One balance or receipt read against a known zkSync Era mainnet address (not an Ethereum or Sepolia address).
eth_estimateGasfor a simple transfer returns a value, so you know estimation, and therefore pubdata-aware gas limits, works on this URL.- If you need push updates, a WebSocket path that survives a disconnect (many public sockets do not).
- Optional: if your wallet or SDK uses the
zks_namespace, callzks_L1BatchNumberandzks_getTransactionDetailson a known hash. A-32601"Method not found" means this URL cannot serve that flow.
Public rows on the hub are shared and rate-limited. Use them for the probe. For production wallet traffic, compare the paid rows on the same hub for method coverage (including zks_*), archive or historical access if you need it, and quota. Dwellir is one of those rows, with HTTPS and WebSocket zkSync Era mainnet endpoints and a flat response-credit model that is easier to budget when your mix of estimates, reads, and sends is not stable. It is not the only option. A cheaper URL that fails the probe above is not a savings. The public vs paid guide covers when a shared endpoint stops being enough, and the pricing calculator helps with the rest. Log-heavy indexers should also read the eth_getLogs caps guide before assuming a wide filter will pass on every URL.
Practical split for a wallet stack
- User-facing zkSync Era row - chain ID 324 /
0x144, ETH symbol,explorer.zksync.io, an RPC you probed today. - zkSync Era Sepolia row - chain ID 300 /
0x12c, a separate config key, never reused as the mainnet RPC. Remove any Goerli (280) leftovers. - Parent Ethereum row - chain ID 1, kept for bridge and withdraw flows, never reused as the Era RPC.
- Signing and history - support EIP-712 type
0x71alongside legacy and EIP-1559, and render paymaster-sponsored transactions correctly. - Fee logic - gas limits from
eth_estimateGason Era plus headroom, priority fee0, no fixed limits carried over from Ethereum. - Backend / indexer - dedicated or paid endpoint with explicit
eth_,zks_, and archive needs; do not share the browser wallet's public URL. - Receipt policy - decide whether "confirmed" means
latest,safe, orfinalizedon Era, remember that L1 finality follows batch proofs and the execution delay, and store that choice next to the RPC URL.
Ship the hub comparison and the probe first. Marketing copy about "zkSync Era RPC" does not replace eth_chainId returning 0x144 and a gas estimate that came from Era.