Message Format

Envelope

Every message from the server is a JSON object with two top-level keys:

{"seq": <u64>, "<MessageType>": <payload>}
  • seq: monotonically increasing server-assigned counter for cursor resume. This is different from seqno inside event payloads (see below). Gaps between consecutive seq values are expected; filtered/blocked events occupy ring slots but are not delivered to the client.
  • <MessageType>: exactly one of the types described below

seq vs seqno: The outer seq is the server-level counter that clients must track for ?resume_from=. The inner seqno (inside each event payload) is the raw event ring position from the validator; it is informational and should not be used for resume logic.

Control frames: Hello, Warning, and Error messages always use seq: 0. Clients must not update their resume cursor from seq: 0 frames; only from seq > 0.

Serialization Rules

  • Field order: seq is always first
  • Numbers: JSON numbers (not strings)
  • Hex types (B256, Address): 0x-prefixed lowercase
  • Null fields: omitted (serde(skip_serializing_if = "Option::is_none"))
  • Byte arrays (Bytes): 0x-prefixed hex string

Server → Client

Hello

Sent before the first Subscribe response. For field descriptions, see Connection.

Events

Batch of execution events:

{
  "seq": 42,
  "Events": [
    {
      "event_name": "BlockStart",
      "block_number": 56147820,
      "txn_idx": null,
      "txn_hash": null,
      "commit_stage": "Proposed",
      "payload": {"type": "BlockStart", "block_number": 56147820, "block_id": "0x..."},
      "seqno": 9876543210,
      "timestamp_ns": 1708345678000000000
    }
  ]
}

Multiple events are grouped into a single Events batch. The server uses drain-based batching: on each processing cycle, all pending events from the broadcast channel are collected and sent as one batch. This means batch size varies, from 1 event during low traffic to hundreds during busy blocks. The order within a batch matches the order events were received from the event ring (execution order).

TPS

{"seq": 43, "TPS": 2450}

ContentionData

{
  "seq": 44,
  "ContentionData": {
    "block_number": 56147820,
    "block_wall_time_ns": 45000000,
    "total_tx_time_ns": 350000000,
    "parallel_efficiency_pct": 87.14,
    "total_unique_slots": 1523,
    "contended_slot_count": 42,
    "contention_ratio": 0.0275,
    "total_txn_count": 150,
    "top_contended_slots": [],
    "top_contended_contracts": [],
    "contract_edges": []
  }
}

TopAccesses

{
  "seq": 45,
  "TopAccesses": {
    "account": [{"key": "0x...", "count": 15234}],
    "storage": [{"key": ["0x...", "0x..."], "count": 8921}]
  }
}

Lifecycle

{
  "seq": 46,
  "Lifecycle": {
    "block_hash": "0x...",
    "block_number": 56147820,
    "from_stage": "Proposed",
    "to_stage": "Voted",
    "time_in_previous_stage_ms": 412.5,
    "block_age_ms": 412.5,
    "txn_count": 150,
    "gas_used": null
  }
}

Warning

Best-effort backpressure notification. Sent every 1,000 drops:

{"seq": 0, "Warning": {"type": "backpressure", "dropped": 1000, "drop_limit": 10000}}

seq: 0: control message, does not contain stream data.

Error

Subscription error. The current subscription remains unchanged:

{"seq": 0, "Error": {"type": "subscribe_limit", "message": "Subscription exceeds limit of 3 items", "limit": 3}}

Error types: subscribe_limit, empty_subscribe, parse_error.

Client → Server

Subscribe (simple)

{"subscribe": ["BlockStart", "BlockFinalized", "TPS"]}

Subscribe (extended)

{
  "subscribe": {
    "events": ["TxnLog"],
    "filters": [{"event_name": "TxnLog", "field_filters": [{"field": "address", "filter": {"values": ["0x..."]}}]}],
    "min_stage": "Finalized",
    "correlate": true
  }
}

Hello (optional)

The client may send Hello for identification. The server logs it but takes no action:

{"hello": {"protocol_version": 1, "client_name": "my-bot", "client_version": "1.0.0"}}

Ping/Pong

Standard WebSocket Ping/Pong frames (binary, protocol-level).

JSON-RPC

The WebSocket also accepts JSON-RPC 2.0 requests. This is a minimal implementation; currently only one method is supported:

MethodDescription
eth_getBlockByNumberReturns the current block number
{"jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": [], "id": 1}

Response:

{"jsonrpc": "2.0", "result": {"number": "0x359a8fc"}, "id": 1}

Unsupported methods return -32601 Method not found. This is not a full Ethereum JSON-RPC implementation. For full RPC support, use a standard Monad node.

Batching

  • Events: grouped into a single Events array per processing cycle (drain-based). Batch size varies from 1 to hundreds depending on throughput.
  • Metrics (TPS, ContentionData, TopAccesses, lifecycle): sent as separate frames, never mixed into the Events array. A metric and an event may share the same seq if they originate from the same ring position.
  • Order within a batch: matches the order of receipt from the event ring, which is the execution order within a block.
  • No backfill: The server only streams live events and replays from the ring buffer. There is no way to request historical events by block range.