SUI MAINNET / A PREMISE SPIKE
One chain.
Four providers.
One answer?
Do Sui mainnet RPC providers disagree on event data? A recorded experiment, examined layer by layer.
The verdict
One question.
One answer.
Four providers. Two recorded runs. No disagreement on completeness or freshness for the pinned queries.
1 The question, and the verdict
Question tested: do Sui mainnet RPC providers return different answers to the same question — event index completeness or freshness?
Verdict: REFUTED. For the pinned queries, the providers do not disagree on completeness or freshness.
REFUTEDTwo runs, >10 minutes apart, each JSON-RPC query issued twice per provider for
self-consistency. Every JSON-RPC provider returned exactly
32 events for the 0x2::coin module query and
1416 for the deny-list event type — identical id sets.
They agreed on the answers, not on speed. Every request's latency was recorded:
Request latency per provider (ms, log scale)
mysten_graphqln=125 · min 267.1 · median 396.8 · max 2,780.1 mspublicnoden=240 · min 113.6 · median 371.4 · max 4,629.4 msblockvisionn=273 · min 27.2 · median 49.4 · max 3,341.7 msrpcpooln=240 · min 22.3 · median 31.2 · max 433.2 mslatency_ms values from the observation rows of both runs (premise_2026-09-27_run1/run2.jsonl.gz), retried transient errors included. One vantage, one day — a snapshot, not a provider benchmark.| Run | Start (UTC) | End (UTC) |
|---|---|---|
| Run 1 | 2026-09-27T10:25:05.105605+00:00 | 2026-09-27T10:32:55.260404+00:00 |
| Run 2 | 2026-09-27T10:43:15.855962+00:00 | 2026-09-27T10:51:35.252748+00:00 |
Vantage local-dev · User-Agent plimmark-sui-spike/0.1
· comparable predicate: MoveEventType (JSON-RPC) == type with fully-qualified type name (GraphQL).
Providers (pinned, no API keys)
| id | endpoint | paradigm |
|---|---|---|
mysten_graphql | https://graphql.mainnet.sui.io/graphql | graphql |
publicnode | https://sui-rpc.publicnode.com | jsonrpc |
blockvision | https://sui-mainnet-endpoint.blockvision.org | jsonrpc |
rpcpool | https://mainnet.sui.rpcpool.com | jsonrpc |
The findings
Below the
surface.
The answers agree. The interfaces do not. Explore the filters, pagination rules, and timestamps behind the result.
2 Findings Sui developers can use today
Mysten's public fullnode JSON-RPC is deprecated
In the recorded check on ,
a JSON-RPC call to fullnode.mainnet.sui.io returned error
-32601 (verbatim):
POST https://fullnode.mainnet.sui.io
{"jsonrpc":"2.0","id":1,"method":"sui_getLatestCheckpointSequenceNumber","params":[]}
✕ error -32601: Method not found. JSON-RPC on public fullnodes has been deprecated. Please migrate to gRPC or GraphQL endpoints. See https://docs.sui.io/develop/accessing-data/json-rpc-migration for more information.Use GraphQL or gRPC. The three third-party JSON-RPC providers below still serve the legacy JSON-RPC surface.
The MoveModule event filter matches the transaction's called module, not the event type
JSON-RPC suix_queryEvents with
MoveModule = {package:0x2, module:coin} returned only
32 events — and their type is
0x2::deny_list::PerTypeConfigCreated, not a coin event. The filter selects
events emitted by transactions whose Move call targets that module, not events of a type
in that module. All three JSON-RPC providers returned the identical 32.
a Move call
0x2::coinwhat MoveModule matches0x2::deny_list::PerTypeConfigCreatedMoveModule = 0x2::coin selects transactions that call that module — the events they emit are deny_list-typed, not coin events.To enumerate a specific event type, filter by its struct type instead:
MoveEventType (JSON-RPC) or type with a fully-qualified type
name (GraphQL) — that query returned 1416 events,
identical across all four providers.
GraphQL's scan-budget trap: 0 nodes + hasNextPage: true does not mean empty
Documented GraphQL behaviour: a forward events scan can return a page
with zero nodes while pageInfo.hasNextPage is true when the
per-request scan budget is exhausted before a match, rather than reaching the end of data.
The client code in this spike is built to handle this behaviour; no such page occurred in
the recorded runs. Keep paginating while the cursor advances;
treat a stalled cursor (no advance) as an incomplete, unknown result — never as an empty set.
Recorded replaynot live · 2026-09-27
hasNextPage: falseReplay of saved responses, not live data. Playback timing is illustrative, not recorded request timing.
29 pages · 28×50 + 16 = 1,416 events ·hasNextPage: false only on page 29hasNextPage: true — none of its committed GraphQL responses has one. Drawn instead: the real Q1b full-history scan (GraphQL events, type 0x2::deny_list::PerTypeConfigCreated, mysten_graphql, run 1, 2026-09-27), one step per page from fixtures/raw/2026-09-27_run1/ 00176–00204 — every page advanced the cursor. The rule for the trap still holds: 0 nodes + hasNextPage: true = not empty; keep paginating while the cursor advances, and treat a stalled cursor as unknown, never zero.Legacy JSON-RPC event timestamps run a fraction of a second late
Across 8 checked transactions, all providers agreed on
which checkpoint contains each transaction. GraphQL's event timestamp equals the
containing checkpoint's own timestamp (the chain's time). Legacy JSON-RPC
suix_queryEvents event timestampMs ran
0.64–0.79 s later than the checkpoint — and later
than the same node's own sui_getTransactionBlock timestamp, which does match.
chain time · t0+639–793 mslegacy JSON-RPC event
suix_queryEvents · late
timestampMs runs 639–793 ms late across all checked transactions. Bound windows by checkpoint, not by time.If you bound an event window by time, the two paradigms will drop different events at the edges even though every event is present in both indexes. Bound by checkpoint, or treat GraphQL's timestamp as ground truth.
Window access on JSON-RPC: compound filters are rejected; cursor seeding works
A compound filter All[MoveEventType, TimeRange] was rejected
(-32602 Invalid params) by all three JSON-RPC providers. Two strategies that
work: seed a descending suix_queryEvents {MoveEventType} scan from a
known event id near the window end (used here), or filter by TimeRange alone
and filter the type client-side.
All[MoveEventType, TimeRange]— blocked:-32602 Invalid paramson all three.- cursor-seed a descending
suix_queryEvents {MoveEventType}scan from the window edge — open channel, accepted on all three.
The method
Same question.
Every time.
Comparable predicates. Exact event identities. Complete pagination. A verdict built from the same test, not assumptions.
3 Method
- Comparable predicate. Compare only what means the same thing on both
paradigms: the event struct type (
MoveEventType== GraphQLtype).MoveModule(called module) is not comparable to GraphQL'smodule(emitting module), so that query is labelled not_comparable, never "different". - Identity match. Event identity is exact across paradigms:
JSON-RPC
(txDigest, eventSeq)== GraphQL(transaction.digest, sequenceNumber). - Self-consistency. Each JSON-RPC query ran twice per provider; a provider disagreeing with itself is flagged and excluded from that query's comparison.
- Two runs. Findings must hold in both runs. Results that did not paginate cleanly are marked unknown (null), never counted as zero.
- Windows. Checkpoint ranges mapped to timestamps; boundary events excluded on all providers. Three 5-minute windows:
| window | checkpoints | start (UTC) |
|---|---|---|
| recent_1h | 327,452,502–327,453,835 | 2026-09-27T08:58:10Z |
| d30 | 315,909,000–315,910,350 | 2026-08-28T09:58:10Z |
| oldest_available | 273,221,834–273,223,147 | 2026-05-07T22:53:11Z |
- Q1b type
0x2::deny_list::PerTypeConfigCreated- Q2 type
0x55300367a2d40813727ccac4ecee977a39fb9cdb46f2e6b2c354b9798f5de2c0::event::PriceFeedUpdateEvent
The limits
Know where
the evidence
ends.
A dated snapshot is not continuous monitoring. Every conclusion has a boundary. These are ours.
4 Limits
- L1A ~20-minute snapshot on 2026-09-27, not continuous monitoring.
- L2A single network vantage (
local-dev). - L3One GraphQL provider (Mysten); GraphQL claims rest on it alone.
- L4The three JSON-RPC providers may run shared indexer software, so their agreement is not four fully independent implementations.
- L5The oldest window held only one matching event, so it is weak evidence on its own.
The evidence
Nothing
without
proof.
The recorded responses, observation rows, and verdict computation remain open to inspection.
5 Evidence
Every claim above is backed by a committed file — the ship's cargo manifest. Raw responses, per-request observation rows (timestamp, provider, latency, bytes, error class + verbatim message), and the verdict computation are all in the repository. Each card links to the file on GitHub.
The correction
Better evidence.
Better answers.
An earlier interpretation was wrong. The record is preserved, the claim corrected, and the evidence left legible.
6 Correction
A premise spike corrects its own record. An earlier reading is struck and amended, the way a ship's log is corrected — the original left legible beneath the line.
Earlier claim: publicnode's 32-event 0x2::coin answer was a partial or stale event index.
amended The 32 events are deny_list::PerTypeConfigCreated, returned identically by all three JSON-RPC providers; MoveModule matches the transaction's called module, not the event type. The genuine type count is 1416, agreed by all four.