monad-grpc API guide for client developers
A practical reference for anyone integrating with monad-grpc-server. Covers what you can subscribe to, how filters compose, how the consensus stages work, and the failure modes you need to handle.
The proto source of truth is monad.proto. When this guide and the proto disagree, trust the proto.
1. Surface area
monad-grpc-server exposes a single gRPC service monad.grpc.v1.Monad with three RPCs:
Plus the standard sidecar services:
grpc.health.v1.Health: Kubernetes-style liveness/readiness probe.grpc.reflection.v1.ServerReflection: schema discovery forgrpcurl-class tools.
If the operator configured auth.x_token, every RPC (including health and reflection) requires the metadata header x-token: <value> or you get UNAUTHENTICATED.
2. Quick start
grpcurl
Tonic (Rust)
3. The mental model
Every subscriber sends a SubscribeRequest containing named filters, a map per event-type. Each named filter is independent; the server delivers an event iff any named filter of the matching type matches it.
The server tags every delivered SubscribeUpdate with the names of the filters that matched, in the filters field. Use these names to route on the client side without re-running the match logic.
An empty SubscribeRequest{} is a null subscription, nothing arrives. Add at least one named filter to receive anything.
The client may resend SubscribeRequest mid-stream to update filters. Each resend completely replaces the previous filter set (except from_seqno, which is only honoured on the first request).
4. Filter types and what they deliver
Within one named filter: AND across populated fields. Empty repeated field is a wildcard.
Across named filters of the same type: OR. The server returns matching names in filters[].
Across filter types: independent. Subscribing to logs does not give you transactions headers.
Hex format for filter values
The server normalises mixed-case input to lowercase. Invalid hex (non-hex characters) and wrong-length values are rejected at Subscribe time with INVALID_ARGUMENT and a clear message, this is intentional fail-fast so a typo doesn't silently produce zero matches.
5. Consensus stages Monad's finality model
Every per-event update carries a stage: BlockStage field, and every named filter can gate by it:
Per-named-filter stage gating
Every Filter* message has two optional stage fields:
Defaults (UNSPECIFIED + empty stages) = "deliver every stage". Per-event events are re-emitted at every stage transition, so a default config delivers the same event up to 4 times tagged with different stages, plus REJECTED on implicit-abandon. The composite UpdateBlock behaves the same 4 composites per block.
Common patterns
min_stage: FINALIZED (a floor, not a whitelist) gives you both FINALIZED and VERIFIED useful when a downstream system can deduplicate by (block_number, log_index).
Stage coverage caveats
txn_rejectsandevm_errorsare emitted only at PROPOSED. The SDK's reassembledExecutedBlockdoes not retain reject/error codes, so we cannot re-emit at later stages. Pair with ablock_meta/transactionsfilter at FINALIZED if you need an accounting-grade boundary.composite UpdateBlock(theblocksmap) is emitted at every stage transition useful for accounting subscribers wanting an atomic block snapshot.
6. Filter messages reference
7. Update messages reference
Every SubscribeUpdate carries:
seqno: uint64- monotonic event-ring seqno (use asfrom_seqnocursor for resume).created_at: google.protobuf.Timestamp- when the server constructed the update.filters: repeated string- the names of every named filter that matched this event.- One of the
update_oneofvariants above.
8. Special features
correlate: true - auto-pull related events
When correlate: true is set on the SubscribeRequest, a UpdateTxnHeader matched by any transactions named filter pulls every other event of the same transaction through the gate, even if the per-event filter wouldn't match it. So:
…delivers UpdateTxnHeader + UpdateTxnReceipt + every UpdateTxnLog / UpdateCallFrame / UpdateAccountAccess / UpdateStorageAccess of those transactions, all tagged with filters: ["watchlist"]. The client routes by name as if every event had matched directly.
The per-block correlation set is bounded and reset on UpdateBlockEnd / UpdateBlockMeta(REJECTED).
Use this when you want atomic per-txn delivery without subscribing to every event type separately.
value_transfers_only: true native ETH transfer monitor
Server delivers only UpdateTxnHeader whose value != "0x0". Catches:
- EOA → EOA transfers
- Contract calls forwarding ETH (
msg.value > 0) - Contract creation with value
UpdateTxnHeader already carries full sender / to / value / nonce / data no separate event type needed. AND-combinable with to: [...] for a wallet watch:
from_seqno replay after disconnect
When set on the first SubscribeRequest of a stream, the server replays all retained SubscribeUpdates with seqno > from_seqno before switching to live broadcast.
Requires the operator to have set replay_buffer_capacity > 0 on the server. Failure modes:
FAILED_PRECONDITION: from_seqno set but replay is disabledoperator didn't enable replay.FAILED_PRECONDITION: from_seqno X is older than retention; reconnect from seqno Y or later, cursor evicted from the in-memory ring; reconnect from the suggested seqno or accept some gap.
from_seqno is honoured only on the first request of a stream. Subsequent updates with from_seqno are silently ignored by the server (with a log warning).
Heartbeat Ping / Pong
Two flavours:
-
Client-driven Ping inside
SubscribeRequest:Server replies with a
Pongcarrying the same nonce inside the same stream. -
Server-driven Ping every
subscriptions.ping_interval_secs(default 10s). Subscribers should treat unknownPingas keepalive.
9. Error codes
The server actively closes the stream after writing the status. The client should treat any of these as "reconnect and resume" except INVALID_ARGUMENT and UNAUTHENTICATED, which require fixing the request.
10. Compression
Standard gRPC negotiation. The server advertises supported codecs via grpc.compression.{accept,send} in its config.
Clients opt in via grpc-accept-encoding: zstd (or gzip). Without that header the stream is uncompressed.
Production indexers typically benefit from enabling zstd (~50% bandwidth reduction on JSON-like proto).
11. Worked examples
A. ERC20 Transfer indexer for one token
You'll get each Transfer event twice (FINALIZED + VERIFIED). Dedupe by (block_number, log_index) or use stages: ["BLOCK_STAGE_FINALIZED"] for exactly-once.
B. Wallet balance monitor
UpdateTxnHeader.filters[] will contain "incoming" or "outgoing"; route accordingly.
C. DEX pool watcher with full execution trace
Every txn to the pool plus its logs, receipts, call frames, and state accesses, atomic per-txn delivery.
D. MEV / mempool monitor real-time + committed in one stream
filters field on each delivery tells you whether it's the speculative or committed copy.
E. Reorg detector
UpdateBlockMeta(stage=REJECTED) carries abandon_reason (EXPLICIT_REJECT, IMPLICITLY_ABANDONED, TIMEOUT); UpdateBlock(stage=REJECTED) gives the full rolled-back content so you can revert any state you wrote.
F. Resume after disconnect
If you get FAILED_PRECONDITION oldest_available is Ntoo far behind, reconnect from N (and accept missing some events) or live-tail.
12. Limits and caveats
- Order between transactions is not guaranteed in raw streams. Within one transaction the server preserves SDK order. Composite
UpdateBlockhas txns sorted bytxn_index. - Events can be lost if the reader (your subscriber) lags too far. The server detects gaps in
seqnoand closes withABORTED. - Speculative blocks can be implicitly abandoned without an explicit reject. Watch
UpdateBlockMeta.abandon_reasonto distinguish. signaturefield onFilterTransactionsmatches againstUpdateTxnHeader.txn_hashonly. Receipts/logs of the same txn requirecorrelate: true.storage_keyfilter onstate_accessonly constrainsUpdateStorageAccess. A pure-address state filter never matches storage updates without a key - by design until a futureaccount_index → addressresolver lands.- Per-deployment
FilterLimitsmay be stricter than defaults. Operators can capmaxnamed filters, list lengths, and reject specific addresses. Read theINVALID_ARGUMENTmessage - it names the specific limit you hit.