Skip to content

Events and indexing

A module records events to tell the outside world what happened: order_placed, bid, owner_set. Events are part of the receipt (events_hash), so validators agree on them like on the output, and they are kept only if the call succeeds.

{"name": "order_placed", "data": {"order": 1842, "amount": 1200}}
  • Name: [A-Za-z0-9_.:-]{1,64}. Data: canonical JSON (no floats, integers within ±(2^53−1)), at most 16 KiB.
  • At most 64 events per call, counting both hive.emit and hive.emit_indexed.
  • A failed call emits nothing.

Today (hive.emit) an app reads events from the call response, or runs its own indexer that parses each event’s JSON. Execution revision 3 adds topics and a hosted index on the Hub.

hive.emit_indexed(name, topics, data) records an event with 0 to 4 topics of 32 bytes each, as in Ethereum logs. Topics are the fields you want to filter by: the buyer, the token id, the order id.

{"name": "bid", "topics": ["0x0000000000000000000000000000000000000000000000000000000000000007"], "data": {"amount": 1200}}

Conventions:

Value Topic
a 32-byte hash or id as is
a number big-endian, left-padded to 32 bytes (lot 7 above)
an EVM address left-padded with zeros to 32 bytes
an ed25519 principal its 32-byte public key
a string keccak256 of its UTF-8 bytes

Principal::topic() (Rust), signer.topic() (JavaScript engine) and hive.principalTopic (AssemblyScript) turn a caller into a topic; TopicFromString / TopicFromAddress (Go) and keccak256 cover the rest.

use hivekit::prelude::*;
let buyer = ctx::caller();
emit_indexed("order_placed", &[buyer.topic(), crypto::keccak256(b"order:1842")], &json!({ "order": 1842, "amount": 1200 }));

Rules:

  • topics_len must be a multiple of 32 and at most 128 bytes (four topics); anything else traps.
  • Name and data follow the hive.emit rules; the 64-events-per-call limit is shared.
  • topics is part of the event, and therefore of events_hash, only when present. Events without topics (all hive.emit events) hash exactly as before.
  • Cost: 500 + 2 per byte of (name + data + topics) + 1,000 per topic. The per-topic charge prices the index entries the event creates on the Hub: use topics for fields people query, put the rest in data.
  • Topics are public, like all state and events. Hash anything private before using it as a topic.

The Hub indexes the events of finalized rounds and serves them by module, name and topic.

GET /v1/module-events?module=0x…&name=order_placed&topic0=0x…&topic1=0x…&after=<cursor>&limit=100
Parameter Meaning
module Required. The module address.
name Optional exact event name.
topic0 … topic3 Optional exact topic at that position, 0x + 64 hex. Filters combine with AND.
after Cursor from a previous page’s next, or an event’s cursor.
limit 1–1000.
{
"events": [
{
"cursor": "opaque",
"module_address": "0x…",
"round_id": "exec:0x…",
"receipt_hash": "0x…",
"finalized_at": 1760000012,
"index": 0,
"name": "order_placed",
"topics": ["0x…"],
"data": {"order": 1842, "amount": 1200},
"origin": "0x9a3c…"
}
],
"next": "opaque or null"
}
  • Order: per module, the order in which the Hub finalized the rounds, then index within the call. No order is defined across modules.
  • origin is the signer of the call from the receipt’s context, "" for unsigned calls. It is the top-level signer, useful for attribution; the module’s own authorization uses ctx.caller().
  • Trust: the Hub is trusted for completeness only, not content. To check an event, fetch GET /v1/rounds/{round_id} for the finality votes, then recompute receipt_hash and events_hash from the receipt and its events. Receipts

GET /v1/module-events/stream takes the same filters and sends matching events as Server-Sent Events as rounds finalize. Each event’s id: is its cursor; reconnecting with Last-Event-ID resumes where you left off. Like /v1/execute, the event API answers web pages on any origin (no cookies). At most 16 open streams per IP.

const url = new URL('https://testnet-rpc.necter.network/v1/module-events/stream')
url.searchParams.set('module', '0x…')
url.searchParams.set('name', 'order_placed')
url.searchParams.set('topic0', buyerTopic)
const es = new EventSource(url)
es.onmessage = (m) => console.log(JSON.parse(m.data)) // the browser resends Last-Event-ID on reconnect

This replaces the “run your own indexer” step of the online store architecture: the backend subscribes to its modules’ events instead of collecting them from call responses.