Tenzro Testnet is live. Get testnet TNZO

Event Streaming

Tenzro provides a unified event streaming system that delivers real-time notifications for every on-chain action across all three VMs (EVM, SVM, DAML). Whether you are building a wallet, an indexer, an AI agent, or a compliance dashboard, the same event bus feeds every consumer through four delivery channels: WebSocket, gRPC streaming, webhooks, and MCP tools. Events are durably stored so consumers can replay history from any point using cursor-based pagination.

Architecture Overview

The event pipeline is structured as a series of layers. At the core sits a lock-free ring buffer inspired by Monad's asynchronous execution design, which decouples event production from consumption so that consensus and execution are never blocked by slow subscribers. Events flow outward through four fan-out channels, each optimized for a different consumer profile. An append-only EventStore (backed by RocksDB column family CF_EVENTS) retains events for historical replay, taking cues from Reth's ExEx framework for reorg-aware delivery and Sui's cursor-based checkpoint model for deterministic replay.

The ring buffer uses a monotonically increasing sequence number (u64) as its cursor. Every event is assigned a sequence at write time, giving consumers a total ordering that survives reconnections. When a chain reorganization occurs, the EventBus emits a ChainReorg event containing the fork point sequence, allowing subscribers to roll back and re-process affected blocks — similar to the approach taken by Solana's Yellowstone gRPC plugin for in-process event delivery.

Event Types

All events share a common envelope containing a sequence number, timestamp, block height, and transaction hash (where applicable). The inner payload is the TenzroEvent enum, which covers every subsystem in the network.

CategoryEventKey Fields
BlockBlockProposedheight, proposer, tx_count, parent_hash
BlockFinalizedheight, state_root, gas_used, elapsed_ms
ChainReorgfork_height, old_head, new_head, depth
TransactionTxSubmittedtx_hash, from, to, value, nonce, vm_type
TxConfirmedtx_hash, block_height, gas_used, status
TxFailedtx_hash, reason, revert_data
EVM LogsEvmLogaddress, topics[], data, log_index
Token / NFTTokenTransfertoken_id, from, to, amount, vm_type
TokenCreatedtoken_id, symbol, decimals, creator
NftMintedcollection, token_id, owner, metadata_uri
IdentityIdentityRegistereddid, identity_type, controller_did
IdentityRevokeddid, reason, cascade_count
CredentialIssuedcredential_type, issuer_did, subject_did, expires_at
AI / AgentInferenceCompletedmodel_id, provider, tokens_used, latency_ms, cost
AgentSpawnedagent_id, template_id, controller_did
AgentMessagefrom_agent, to_agent, message_type, payload_hash
SettlementSettlementCompletedsettlement_id, payer, payee, amount, protocol
ChannelOpenedchannel_id, parties, deposit, ttl
StakingStakeDepositedvalidator, amount, total_stake, role
ValidatorSlashedvalidator, slash_amount, reason, evidence_hash
ProposalCreatedproposal_id, proposer, title, voting_end
BridgeBridgeInitiatedbridge_id, src_chain, dst_chain, amount, adapter
BridgeCompletedbridge_id, dst_tx_hash, elapsed_secs

Delivery Channels

Tenzro exposes the same events through four delivery channels, each suited to different latency requirements and integration patterns.

ChannelProtocolTypical ConsumerLatencyEndpoint
WebSocketJSON-RPC 2.0 over WSWallets, dApps, explorers~5 msws://localhost:8545
gRPC StreamingBidirectional ProtobufIndexers, validators, analytics~3 mslocalhost:3008
WebhooksHTTP POST + HMAC-SHA256Bots, backends, alerting100 ms - 5 sConfigured via RPC
MCP ToolsJSON-RPC over HTTPAI agents, LLM integrations~10 mslocalhost:3001/mcp

All four channels share the same event sequence numbering, so a consumer can switch delivery methods without missing events by providing the last-seen sequence as a cursor.

WebSocket Subscriptions

The WebSocket interface is fully compatible with the Ethereum eth_subscribe specification, so existing tools like viem, ethers.js, and web3.js work without modification. Tenzro extends the standard subscription types with a tenzroEvents type for cross-VM event streaming.

Subscribe to New Blocks

// Request — subscribe to new block headers
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_subscribe",
  "params": ["newHeads"]
}

// Response — subscription ID
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": "0xa1b2c3d4e5f60001"
}

// Notification — pushed on each new block
{
  "jsonrpc": "2.0",
  "method": "eth_subscription",
  "params": {
    "subscription": "0xa1b2c3d4e5f60001",
    "result": {
      "number": "0x1f4",
      "hash": "0xabc...def",
      "parentHash": "0x123...789",
      "stateRoot": "0xfed...321",
      "timestamp": "0x66a2b3c4",
      "gasUsed": "0xe4e1c0",
      "transactionCount": 42
    }
  }
}

Subscribe to EVM Logs

// Subscribe to Transfer events on the wTNZO ERC-20 contract
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "eth_subscribe",
  "params": [
    "logs",
    {
      "address": "0x7a4bcb13a6b2b384c284b5caa6e5ef3126527f93",
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
      ]
    }
  ]
}

Subscribe to Tenzro Events

// Subscribe to settlement and inference events across all VMs
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "eth_subscribe",
  "params": [
    "tenzroEvents",
    {
      "event_types": ["SettlementCompleted", "InferenceCompleted"],
      "from_sequence": 50000
    }
  ]
}

// Notification — settlement event
{
  "jsonrpc": "2.0",
  "method": "eth_subscription",
  "params": {
    "subscription": "0xa1b2c3d4e5f60003",
    "result": {
      "sequence": 50142,
      "event_type": "SettlementCompleted",
      "block_height": 512,
      "timestamp": 1720300800,
      "data": {
        "settlement_id": "settle_abc123",
        "payer": "0x1234...abcd",
        "payee": "0x5678...ef01",
        "amount": "1500000000000000000",
        "protocol": "mpp"
      }
    }
  }
}

Supported subscription types: newHeads, logs, newPendingTransactions, syncing, and tenzroEvents. Use eth_unsubscribe to cancel a subscription.

gRPC Streaming

For high-throughput consumers like indexers and analytics pipelines, the gRPC streaming interface on port 3008 provides the lowest latency (~3 ms) with Protobuf-encoded events. The service supports bidirectional streaming, allowing clients to dynamically update their filter without dropping the connection.

// Proto definition (proto/tenzro/v1/events.proto)
service EventStream {
  // Server-streaming: client sends a filter, server pushes matching events
  rpc Subscribe (EventFilter) returns (stream EventEnvelope);

  // Bidirectional: client can update filters mid-stream
  rpc SubscribeDynamic (stream EventFilter) returns (stream EventEnvelope);

  // Unary: fetch a page of historical events
  rpc GetEvents (GetEventsRequest) returns (GetEventsResponse);
}

message EventFilter {
  repeated string event_types = 1;   // e.g. ["BlockFinalized", "TokenTransfer"]
  repeated string addresses = 2;     // filter by involved address
  repeated bytes  topics = 3;        // EVM log topic0 filters
  repeated string vm_types = 4;      // "evm", "svm", "daml"
  uint64 from_sequence = 5;          // cursor for replay
}

message EventEnvelope {
  uint64 sequence = 1;
  uint64 timestamp = 2;
  uint64 block_height = 3;
  bytes  tx_hash = 4;
  TenzroEvent event = 5;
}

Rust Client Example

use tenzro_sdk::events::EventStreamClient;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut client = EventStreamClient::connect("http://localhost:3008").await?;

    let filter = EventFilter {
        event_types: vec!["TokenTransfer".into(), "SettlementCompleted".into()],
        from_sequence: 0,  // start from the beginning
        ..Default::default()
    };

    let mut stream = client.subscribe(filter).await?.into_inner();

    while let Some(envelope) = stream.message().await? {
        println!(
            "seq={} height={} event={:?}",
            envelope.sequence, envelope.block_height, envelope.event
        );
    }

    Ok(())
}

The from_sequence field enables cursor-based replay. When a consumer reconnects after a disconnect, it passes its last processed sequence number and receives all subsequent events without gaps. The EventStore retains events for a configurable retention period (default: 7 days, or 10 million events, whichever comes first).

Webhooks

Webhooks deliver events as HTTP POST requests to a URL you control. Every webhook payload is signed with HMAC-SHA256 so your backend can verify authenticity. Tenzro supports dual delivery: unconfirmed events are sent immediately for low-latency alerting, and confirmed events are sent after finalization for reliable processing.

Register a Webhook

// Register via JSON-RPC
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tenzro_registerWebhook",
  "params": [{
    "url": "https://api.example.com/tenzro/events",
    "secret": "whsec_a1b2c3d4e5f6...",
    "filter": {
      "event_types": ["TxConfirmed", "ValidatorSlashed"],
      "addresses": ["0x1234...abcd"]
    },
    "delivery": "confirmed",
    "max_retries": 5
  }]
}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "webhook_id": "wh_7f8g9h0i",
    "status": "active",
    "created_at": 1720300800
  }
}

Webhook Payload Format

// HTTP POST to your endpoint
// Headers:
//   Content-Type: application/json
//   X-Tenzro-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015...
//   X-Tenzro-Webhook-Id: wh_7f8g9h0i
//   X-Tenzro-Timestamp: 1720300800
//   X-Tenzro-Sequence: 50142

{
  "webhook_id": "wh_7f8g9h0i",
  "sequence": 50142,
  "timestamp": 1720300800,
  "delivery": "confirmed",
  "event": {
    "type": "TxConfirmed",
    "block_height": 512,
    "tx_hash": "0xabc...def",
    "data": {
      "from": "0x1234...abcd",
      "to": "0x5678...ef01",
      "value": "1000000000000000000",
      "gas_used": 21000,
      "status": "success"
    }
  }
}

To verify the signature, compute HMAC-SHA256(secret, timestamp + "." + body) and compare against the X-Tenzro-Signature header value. Deliveries that receive a non-2xx response are retried with exponential backoff (1s, 2s, 4s, 8s, 16s) up to the configured max_retries. After all retries are exhausted, the webhook is marked as degraded and an alert is emitted.

Event Filtering

All delivery channels accept the same EventFilter structure. The filter is compatible with Ethereum's eth_getLogs address and topic filtering, extended with Tenzro-specific fields for cross-VM event selection.

// EventFilter structure
{
  // Ethereum-compatible fields
  "address": "0x7a4b...7f93" | ["0x7a4b...7f93", "0xdead...beef"],
  "topics": [
    "0xddf252ad...",           // topic0: event signature
    null,                      // topic1: any value (wildcard)
    "0x00000000...1234abcd"    // topic2: specific indexed param
  ],
  "fromBlock": "0x100",        // start block (inclusive)
  "toBlock": "latest",         // end block (inclusive)

  // Tenzro extensions
  "event_types": ["TokenTransfer", "InferenceCompleted"],
  "vm_types": ["evm", "svm"],
  "from_sequence": 50000,      // cursor-based replay start
  "limit": 1000                // max events per response
}

Filters are evaluated as a conjunction: all specified conditions must match for an event to be delivered. Within array fields, values are disjunctive (any match suffices). For example, specifying both event_types: ["TokenTransfer", "NftMinted"] and vm_types: ["evm"] returns EVM token transfers and EVM NFT mints, but not SVM token transfers.

Historical Queries

Historical events can be retrieved through the standard eth_getLogs RPC for EVM log queries, or through the Tenzro-specific tenzro_getEvents method for cross-VM event replay with cursor-based pagination.

// eth_getLogs — standard EVM log query
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_getLogs",
  "params": [{
    "fromBlock": "0x1",
    "toBlock": "latest",
    "address": "0x7a4bcb13a6b2b384c284b5caa6e5ef3126527f93",
    "topics": [
      "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
    ]
  }]
}

// tenzro_getEvents — cross-VM historical query with cursor pagination
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tenzro_getEvents",
  "params": [{
    "event_types": ["SettlementCompleted", "BridgeCompleted"],
    "from_sequence": 0,
    "limit": 100
  }]
}

// Response
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "events": [
      {
        "sequence": 1042,
        "event_type": "SettlementCompleted",
        "block_height": 100,
        "timestamp": 1720200000,
        "data": { ... }
      }
    ],
    "next_cursor": 1142,
    "has_more": true
  }
}

To page through results, pass the next_cursor value as from_sequence in the next request. Continue until has_more is false.

CLI Usage

The tenzro events command group provides a convenient interface for subscribing to live events, querying history, and managing webhooks from the terminal.

# Subscribe to all events in real-time (WebSocket)
tenzro events subscribe

# Subscribe with filters
tenzro events subscribe --types TokenTransfer,InferenceCompleted --vm evm

# Resume from a known sequence (cursor-based recovery)
tenzro events subscribe --from-sequence 50000

# Query historical events
tenzro events history --types SettlementCompleted --limit 50

# Query EVM logs for a specific contract
tenzro events logs \
  --address 0x7a4bcb13a6b2b384c284b5caa6e5ef3126527f93 \
  --topic0 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef \
  --from-block 1 --to-block latest

# Register a webhook
tenzro events register-webhook \
  --url https://api.example.com/events \
  --secret whsec_mySecret123 \
  --types TxConfirmed,ValidatorSlashed \
  --delivery confirmed

# List active webhooks
tenzro events info

# Output as JSON for piping to jq
tenzro events subscribe --types BlockFinalized --format json | jq '.block_height'

Best Practices

Polling vs. WebSocket

While WebSocket subscriptions offer the lowest latency, libraries like viem default to polling for reliability. For most dApp use cases, polling eth_getFilterChanges every 4 seconds is sufficient and avoids the complexity of managing reconnection logic. Reserve WebSocket subscriptions for latency-sensitive applications like trading bots or real-time dashboards.

Cursor-Based Recovery

Always persist the last-processed sequence number in your consumer. On reconnect, pass it as from_sequence to resume exactly where you left off. This guarantees at-least-once delivery without requiring the server to track consumer state. Consumers should be idempotent — use the sequence number or tx_hash as a deduplication key.

HMAC Verification

Always verify the X-Tenzro-Signature header on webhook payloads using a constant-time comparison. Check the X-Tenzro-Timestamp to reject payloads older than 5 minutes, preventing replay attacks. Never log the webhook secret or include it in error responses.

Rate Limiting

The WebSocket and gRPC interfaces enforce per-connection rate limits. WebSocket subscriptions are capped at 10,000 events per second per connection. If your consumer falls behind, events are buffered up to the ring buffer capacity (65,536 events), after which the oldest undelivered events are dropped and a SubscriptionOverflow notification is sent with the gap range. Use from_sequence to recover the missed events from the EventStore.

Handling Chain Reorganizations

When a ChainReorg event arrives, roll back any local state derived from blocks above the fork_height and re-process events from that point. For webhooks, the confirmed delivery mode avoids this entirely by only sending events after finalization. For WebSocket and gRPC consumers that need instant updates, subscribe to ChainReorg alongside your primary events and implement rollback logic.