Neutron is not an EVM chain, and the RPC reflects that: there is no eth_* surface at all, no numeric chain ID, and the request shape is CometBFT JSON-RPC rather than the Ethereum method set. None of that is surprising once you have read a Cosmos RPC reference — but each of these will break a piece of code you already have, and a few of them will not break it loudly.
Everything below was measured on 2026-09-20 through https://rpc.swiftnodes.io/rpc/neutron, against the same chain from a second independent endpoint for anything I did not want to take on trust. Re-run any of it.
The essentials
| Network / chain ID |
neutron-1 (a string — there is no numeric chain ID) |
| Consensus client | CometBFT 0.38.19 (protocol p2p 8, block 11, app 0) |
| Block time | ~7.0 s measured this week, ~2.05 s over the preceding months — see below |
| Head at time of writing | 61,612,831 |
| Validators | 11 active, 11 signatures in last_commit, ed25519
|
| Block limits |
max_bytes 22,020,096 (~21 MiB), max_gas 330,000,000 |
| EVM methods |
None. eth_chainId → -32601 Method not found
|
| HTTP | https://rpc.swiftnodes.io/rpc/neutron?key=YOUR_API_KEY |
| Execution layer | CosmWasm (wasmvm) contracts, not the EVM |
Connecting: the setup
CometBFT RPC is JSON-RPC 2.0 over POST, with the method name in the body. There are no params arrays in the Ethereum sense — most methods take an object, and many take nothing.
curl
# node + sync state
curl -s -X POST https://rpc.swiftnodes.io/rpc/neutron?key=YOUR_API_KEY \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"status","params":{}}'
# a specific block by height
curl -s -X POST https://rpc.swiftnodes.io/rpc/neutron?key=YOUR_API_KEY \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"block","params":{"height":"61612831"}}'
The one thing to get right early: this path is POST-only. A GET /status — which works against a bare CometBFT node on port 26657 — returns 404 here. Clients built around the Tendermint REST-style paths will need the JSON-RPC-over-POST form instead.
A tiny fetch helper
const RPC = "https://rpc.swiftnodes.io/rpc/neutron?key=YOUR_API_KEY";
async function rpc(method, params = {}) {
const r = await fetch(RPC, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
});
const j = await r.json();
if (j.error) throw new Error(`${method}: ${j.error.message} ${j.error.data ?? ""}`.trim());
return j.result;
}
const s = await rpc("status");
const height = Number(s.sync_info.latest_block_height); // 61612831 at time of writing
const block = await rpc("block", { height: String(height) });
console.log(block.block.header.chain_id); // "neutron-1"
Note the explicit error check. CometBFT signals most failures as a JSON-RPC error object with HTTP 200, so a client that only looks at status codes will treat "height is not available" as a successful response with a missing field.
CosmJS users: Tendermint37Client / HttpBatchLink speak this same POST-to-root JSON-RPC, so the endpoint drops in. Verify the version handshake yourself rather than assuming — this node reports CometBFT 0.38.19, and the client you pick has to tolerate that.
What's different from an EVM chain
There is no chain ID number
eth_chainId returns -32601 Method not found, and our own status API reports chainId: null for this chain. The identifier is the string neutron-1, and it lives in status.node_info.network and in every block header. Anything in your code that expects an integer — a switch (chainId) on a config map, a wallet addEthereumChain call, a chain-registry lookup keyed by number — needs a string key here.
Two header fields vanished in CometBFT 0.38
The header I read at block 61,612,809 had exactly these keys:
version, chain_id, height, time, last_block_id, last_commit_hash, data_hash,
validators_hash, next_validators_hash, consensus_hash, app_hash,
last_results_hash, evidence_hash, proposer_address
There is no num_txs and no total_txs — 0.38 removed them. Code that reads header.num_txs gets undefined, which in JavaScript means NaN in arithmetic and a silently empty loop in a "skip empty blocks" filter. The replacement is the transaction list itself: result.block.data.txs.length (the block I sampled contained 1 tx).
health returns an empty object
{"jsonrpc":"2.0","id":1,"result":{}}
No is_node_healthy field, from either this route or the upstream directly — so a readiness probe that checks result.is_node_healthy === true will read undefined and fail, and one that checks result.is_node_healthy !== false will pass on a node that is down. Use status.sync_info.catching_up === false instead, and treat a failed status call as unhealthy. status.validator_info also came back null here, so don't build on it either.
Block production is not what the docs say
This is the one I would not have believed without measuring twice.
| Window | Measured |
|---|---|
| last 100,000 blocks (2026-09-12 → 09-20) | 7.02 s / block |
| last 5,000 blocks | 7.001 s |
| last 1,000 blocks | 6.995 s |
| last 200 blocks | 6.979 s |
| block 56,000,000 (2026-05-10) → head | 2.05 s / block |
Consecutive blocks were 7.40, 1.56, 7.41, 7.72, 7.45, 7.44 s apart — so this is a steady ~7 s cadence with occasional fast blocks, not one slow block skewing an average. I repeated the same measurement against a second, independent RPC and got 6.979 s and 7.001 s for the 200- and 2000-block windows, which rules out anything about the route in between.
Read that as: Neutron's long-run rate is ~2 s, and as of this week it is producing at roughly 7 s. Anything you have tuned against a 2 s assumption — a poll interval, a "stale tip" alert threshold, a timeout budget, a UI's "synced" indicator, an ETA in blocks — is currently wrong by about 3.5×. If you have a fixed-interval poller, make it interval-based rather than block-count-based, and set the stall threshold from a measurement you take today, not from a spec.
History: the floor moves between nodes
This is the part that will bite you in production.
Requesting block at height 1 does not return an empty result — it returns an explicit refusal naming the node's own retention floor:
{"code":-32603,"message":"Internal error",
"data":"height 1 is not available, lowest height is 55481501"}
Then, minutes later, the same request through the same URL named a different floor: 61317000. And block at 61,317,000 failed while 61,317,001 succeeded.
That is not flakiness in the transport. A pool of independent RPC nodes each prune on their own schedule, so "how far back can I read?" has no single answer on a load-balanced endpoint. Observed floors on this route today spanned 55,481,501 to 61,317,000 — a difference of ~5.8 million blocks, i.e. one node kept months more history than another.
Practical consequences:
- A historical read may succeed and then fail on the next identical call. Design for it: retry, and if the query matters, pin it to a specific archival endpoint rather than a pooled one.
- Do not infer "archive" from a successful old-block read. One success means one node had it.
-
?archive=1is an EVM routing hint on SwiftNodes; it does not apply to CometBFT chains like this one.
For reference, the genesis method reports genesis_time: 2025-11-28T11:42:18Z with initial_height: 1 — which cannot be reconciled with a chain at height 61.6M. Treat the genesis result as a consensus-parameter document (chain ID, validator set, upgrade heights), not as a reliable chain birthday. A third endpoint I probed returned no genesis at all.
The methods you'll actually use
| Method | What it gives you | Notes |
|---|---|---|
status |
network, CometBFT version, head height/time, catching_up
|
your real health check |
block |
header + txs + last_commit for one height |
params.height as a string
|
block_results |
events and per-tx results for a height | separate call from block
|
blockchain |
compact metadata for a height range | cheap way to walk history |
tx |
one transaction by hash, with proof | |
tx_search |
find txs by query | see the warning below |
validators |
the set at a height |
total came back 11 |
consensus_params |
block limits, evidence rules, vote-extensions height | |
broadcast_tx_sync / _async / _commit
|
submit a tx |
_commit waits for inclusion |
abci_query |
raw app state (contracts, params, stores) | the Cosmos equivalent of a storage read |
block and block_results are two calls. Events — the thing you usually actually want — live in block_results, so a poller that fetches only block sees transactions with no results and reports nothing happened.
tx_search: some queries do not answer
| Query | Result |
|---|---|
tx.height=61612805 |
129 ms, total_count=1
|
message.action='/cosmwasm.wasm.v1.MsgExecuteContract' |
17,245 ms, total_count=47532
|
tx.height>0 |
28,066 ms, then Backend node error
|
Exact-height lookups are fast. Attribute searches are served, but slowly and at large result counts, and an unbounded range query does not return at all — it burns ~28 s and then errors. If you need "all txs matching X over a period", iterate strict heights or use an indexer; do not hand tx_search a wide range and wait.
On the dYdX route we also saw a stricter rule — tx.height>0 rejected outright with -32701 "please specify tx.height event with strict equality". Different operators configure the indexer differently, so treat tx_search as capability you must probe per endpoint, not a guarantee of the RPC spec.
WebSocket
wss is available on this chain and our status API reports wsAvailable: true. The subscription handshake is CometBFT's, not Ethereum's: method subscribe with query, e.g. {"jsonrpc":"2.0","id":1,"method":"subscribe","params":{"query":"tm.event='NewBlockEvent'"}}, and events arrive as notifications on that subscription rather than as eth_subscribe IDs. If your code already handles eth_subscribe, this is a rewrite of the subscription layer, not a config change.
What this means for your stack
Check your assumptions about time. ~7 s blocks against a 2 s baseline changes every block-count-derived number in your system: staleness thresholds, retry backoffs, "confirmations", and how long a poll interval can be before you look dead.
Stop reading num_txs. It is gone in 0.38 and its absence is silent. Use data.txs.length.
Treat health as unusable on this surface and gate readiness on status.sync_info.catching_up.
Assume history is partial. Ask the node what its floor is by requesting height 1 and reading the error, and do that at startup rather than discovering it on a user-facing query.
Verify capability by calling it. eth_chainId, tx_search ranges and old-block reads are three places where this chain told the truth only when asked directly. SwiftNodes publishes a weekly-measured method-support matrix for exactly this reason — the EVM half of it is at Method support, and the full chain list is at /chains.
The short version
Neutron speaks CometBFT 0.38.19 JSON-RPC over POST on chain neutron-1, with 11 validators, ~21 MiB blocks and a max_gas of 330,000,000. There is no numeric chain ID and no eth_* surface at all; num_txs and total_txs are gone from the header in 0.38; health returns an empty object so readiness has to come from status; block and block_results are separate calls; wide tx_search queries time out where exact-height ones answer in ~130 ms; and the pruning floor differs between nodes in the same pool (55.48M to 61.32M observed today), so historical reads are not reliable on a pooled endpoint. The number to watch: blocks measured 7.02 s over the last 100k, against a 2.05 s average across the preceding five million — a live 3.5× slowdown, confirmed against a second independent RPC.
For Neutron RPC across load-balanced independent upstreams, with HTTP, gRPC, REST and WebSocket, grab a free API key and point your client at:
https://rpc.swiftnodes.io/rpc/neutron?key=YOUR_API_KEY
One key covers all 86 chains, and the free tier is 2 requests per second — enough to run every probe above, including the block-time windows, and check this week's numbers for yourself.
Top comments (0)