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.
| 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 |
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== 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.
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.
- Verdict, comparison tables, addendum — fixtures/premise_2026-09-27.md
- Run 1 observations + results (gz) — fixtures/premise_2026-09-27_run1.jsonl.gz
- Run 2 observations + results (gz) — fixtures/premise_2026-09-27_run2.jsonl.gz
- Pinned query set + windows — spike_config.json
- Filter semantics + comparability evidence — fixtures/semantics_2026-09-27.md
- Timestamp ground truth — fixtures/timestamps_2026-09-27.md
- GraphQL schema introspection — fixtures/graphql_introspection_2026-09-27.json
- Mysten fullnode JSON-RPC -32601 (verbatim) — fixtures/mysten_fullnode_jsonrpc_2026-09-27.md
- Corrections — fixtures/corrections_2026-09-27.md
- Public-release scrub — fixtures/public_scrub_2026-09-27.md
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.