Plimsmark

Do Sui mainnet RPC providers disagree on event data? A dated, network-tested spike.

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. Two runs, >10 minutes apart, each JSON-RPC query issued twice per provider for self-consistency.

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

2. Findings Sui developers can use today

Mysten's public fullnode JSON-RPC is deprecated

A JSON-RPC call to fullnode.mainnet.sui.io returns error -32601 (verbatim):

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. 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

A forward GraphQL events scan can return a page with zero nodes while pageInfo.hasNextPage is true: the per-request scan budget was exhausted before a match, not the end of data. Keep paginating while the cursor advances; treat a stalled cursor (no advance) as an incomplete, unknown result — never as an empty set.

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. 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.

3. Method

  • 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.

4. Limits

  • A ~20-minute snapshot on 2026-09-27, not continuous monitoring.
  • A single network vantage (local-dev).
  • One GraphQL provider (Mysten); GraphQL claims rest on it alone.
  • The three JSON-RPC providers may run shared indexer software, so their agreement is not four fully independent implementations.
  • The oldest window held only one matching event, so it is weak evidence on its own.

5. Evidence

Every claim above is backed by a committed file. Raw responses, per-request observation rows (timestamp, provider, latency, bytes, error class + verbatim message), and the verdict computation are all in the repository.

6. Correction

An earlier spike read publicnode's 32-event 0x2::coin answer as a partial or stale index. That is refuted here: the 32 events are deny_list::PerTypeConfigCreated, returned identically by all three JSON-RPC providers; MoveModule matches the called module, not the event type. See corrections_2026-09-27.md.