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.emitandhive.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.
Indexed events: hive.emit_indexed
Section titled “Indexed events: hive.emit_indexed”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 }));buyer := hivekit.Caller()hivekit.EmitIndexed("order_placed", [][32]byte{buyer.Topic(), hivekit.TopicFromString("order:1842")}, map[string]any{"order": 1842, "amount": 1200})const buyer = hive.principalTopic(hive.caller())!hive.emitIndexed('order_placed', [buyer], '{"amount":1200,"order":1842}')const buyer = hive.Principal.parse(ctx.caller)ctx.emit('order_placed', { order: 1842, amount: 1200 }, { topics: [buyer.topic()] })buyer = Principal.parse(ctx.caller)hive.emit("order_placed", {"order": 1842, "amount": 1200}, topics=[buyer])Rules:
topics_lenmust be a multiple of 32 and at most 128 bytes (four topics); anything else traps.- Name and data follow the
hive.emitrules; the 64-events-per-call limit is shared. topicsis part of the event, and therefore ofevents_hash, only when present. Events without topics (allhive.emitevents) 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.
Querying events from the Hub
Section titled “Querying events from the Hub”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
indexwithin the call. No order is defined across modules. originis 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 usesctx.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 recomputereceipt_hashandevents_hashfrom the receipt and its events. Receipts
Live stream
Section titled “Live stream”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 reconnectThis 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.