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.

Explore the findings Recorded verdict REFUTED
FIELD REPORT / 2026-09-27Visual metaphor · not a measurement

The verdict

One question.
One answer.

Four providers. Two recorded runs. No disagreement on completeness or freshness for the pinned queries.

01 / AGREEMENTVisual metaphor · not a measurement

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.

REFUTED
mysten_graphqlgraphqlpublicnodejsonrpcblockvisionjsonrpcrpcpooljsonrpc
Four providers, one waterline: identical id sets, level trim. No ship rides lower than the others.

Two 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 ms
mysten_graphql: n=125 · min 267.1 · median 396.8 · max 2,780.1 ms; middle half 347.8–951.4 ms
publicnoden=240 · min 113.6 · median 371.4 · max 4,629.4 ms
publicnode: n=240 · min 113.6 · median 371.4 · max 4,629.4 ms; middle half 307.6–1,074.8 ms
blockvisionn=273 · min 27.2 · median 49.4 · max 3,341.7 ms
blockvision: n=273 · min 27.2 · median 49.4 · max 3,341.7 ms; middle half 39.6–111.2 ms
rpcpooln=240 · min 22.3 · median 31.2 · max 433.2 ms
rpcpool: n=240 · min 22.3 · median 31.2 · max 433.2 ms; middle half 28.6–52.2 ms
All 878 recorded latency_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.
RunStart (UTC)End (UTC)
Run 12026-09-27T10:25:05.105605+00:002026-09-27T10:32:55.260404+00:00
Run 22026-09-27T10:43:15.855962+00:002026-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)

idendpointparadigm
mysten_graphqlhttps://graphql.mainnet.sui.io/graphqlgraphql
publicnodehttps://sui-rpc.publicnode.comjsonrpc
blockvisionhttps://sui-mainnet-endpoint.blockvision.orgjsonrpc
rpcpoolhttps://mainnet.sui.rpcpool.comjsonrpc

The findings

Below the
surface.

The answers agree. The interfaces do not. Explore the filters, pagination rules, and timestamps behind the result.

02 / INTERFACESVisual metaphor · not a measurement

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):

Legacy JSON-RPCRecorded response
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.
Archived evidence · — not a live request or a website error. The endpoint rejected legacy JSON-RPC; the spike uses GraphQL. View recorded source.

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.

transaction
a Move call
calls →
called module
0x2::coinwhat MoveModule matches
emits ↓
event type
0x2::deny_list::PerTypeConfigCreated
MoveModule = 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

05001,0001,416page 1: 50 nodes, 50 cumulative, hasNextPage truepage 2: 50 nodes, 100 cumulative, hasNextPage truepage 3: 50 nodes, 150 cumulative, hasNextPage truepage 4: 50 nodes, 200 cumulative, hasNextPage truepage 5: 50 nodes, 250 cumulative, hasNextPage truepage 6: 50 nodes, 300 cumulative, hasNextPage truepage 7: 50 nodes, 350 cumulative, hasNextPage truepage 8: 50 nodes, 400 cumulative, hasNextPage truepage 9: 50 nodes, 450 cumulative, hasNextPage truepage 10: 50 nodes, 500 cumulative, hasNextPage truepage 11: 50 nodes, 550 cumulative, hasNextPage truepage 12: 50 nodes, 600 cumulative, hasNextPage truepage 13: 50 nodes, 650 cumulative, hasNextPage truepage 14: 50 nodes, 700 cumulative, hasNextPage truepage 15: 50 nodes, 750 cumulative, hasNextPage truepage 16: 50 nodes, 800 cumulative, hasNextPage truepage 17: 50 nodes, 850 cumulative, hasNextPage truepage 18: 50 nodes, 900 cumulative, hasNextPage truepage 19: 50 nodes, 950 cumulative, hasNextPage truepage 20: 50 nodes, 1000 cumulative, hasNextPage truepage 21: 50 nodes, 1050 cumulative, hasNextPage truepage 22: 50 nodes, 1100 cumulative, hasNextPage truepage 23: 50 nodes, 1150 cumulative, hasNextPage truepage 24: 50 nodes, 1200 cumulative, hasNextPage truepage 25: 50 nodes, 1250 cumulative, hasNextPage truepage 26: 50 nodes, 1300 cumulative, hasNextPage truepage 27: 50 nodes, 1350 cumulative, hasNextPage truepage 28: 50 nodes, 1400 cumulative, hasNextPage truepage 29: 16 nodes, 1416 cumulative, hasNextPage false1102029page (first: 50)cumulative events
Page 29 / 291,416 events+16 this pagehasNextPage: false

Replay 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 29
Substitute, not the trap itself: this spike never recorded a page with 0 nodes + hasNextPage: 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.

checkpoint = GraphQL event
chain time · t0
+639–793 mslegacy JSON-RPC event
suix_queryEvents · late
GraphQL matches the chain; the legacy JSON-RPC 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 params on 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.

03 / METHODVisual metaphor · not a measurement

3 Method

providers4 endpointspaginationcursor to endidentity match(txDigest, seq)self-consistency×2 per queryverdictREFUTED
Each query runs the full pipeline; a stage that cannot complete cleanly yields unknown (null), never zero.
  • Comparable predicate. Compare only what means the same thing on both paradigms: the event struct type (MoveEventType == GraphQL type). MoveModule (called module) is not comparable to GraphQL's module (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:
windowcheckpointsstart (UTC)
recent_1h327,452,502–327,453,8352026-09-27T08:58:10Z
d30315,909,000–315,910,3502026-08-28T09:58:10Z
oldest_available273,221,834–273,223,1472026-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.

04 / BOUNDARIESVisual metaphor · not a measurement

4 Limits

  1. L1A ~20-minute snapshot on 2026-09-27, not continuous monitoring.
  2. L2A single network vantage (local-dev).
  3. L3One GraphQL provider (Mysten); GraphQL claims rest on it alone.
  4. L4The three JSON-RPC providers may run shared indexer software, so their agreement is not four fully independent implementations.
  5. L5The oldest window held only one matching event, so it is weak evidence on its own.
Like a ship's load line, each mark is a limit on how heavily these findings can be loaded — read below the line, not above it.

The evidence

Nothing
without
proof.

The recorded responses, observation rows, and verdict computation remain open to inspection.

05 / PROVENANCEVisual metaphor · not a measurement

The correction

Better evidence.
Better answers.

An earlier interpretation was wrong. The record is preserved, the claim corrected, and the evidence left legible.

06 / CORRECTIONVisual metaphor · not a measurement

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.

See corrections_2026-09-27.md.