Protocol · Composition
Building new events from finalized ones
EventComposer builds composite events from primitive (or other composite) event IDs, deterministically, without duplicating Registry state. A composite is itself just another finalized event — composable again, readable by any application.
| Operator | Operands | Rule |
|---|---|---|
| AND | 2+ | True iff every operand resolved true. |
| OR | 2+ | True iff any operand resolved true. |
| NOT | 1 | Negates its single operand. |
| BEFORE | 2 | True iff both resolved true and operand 0 finalized strictly before operand 1. |
| WITHIN | 2 + window | True iff both resolved true and their finalization timestamps differ by at most `window` seconds (inclusive). |
| K_OF_N | — | Coming soon — explicitly out of scope for this pass (SPEC_AND_TASKS.md Issue #10). Not implemented, not deployed. |
Canonical identity
Operand order is canonicalized (sorted ascending) for every order-independent operator — AND, OR, NOT, WITHIN — so two apps independently composing AND(a, b) and AND(b, a) land on the same on-chain object. BEFORE is the deliberate exception: “A before B” is not the same claim as “B before A”, so its operand order is preserved exactly as given.
Example
The canonical demo composes a rate-decision event and a price-threshold event with WITHIN(48h):
const compositeId = await client.createComposite(
{
op: CompositeOp.Within,
operands: [rateDecisionEventId, priceThresholdEventId],
window: 48n * 60n * 60n,
},
account,
);Structural bounds
Composition depth is capped at 8, and fan-in (operands per composite) at 10 — enforced at creation time. Cycles are not just bounded, they are cryptographically impossible: a composite's ID is a hash of its own operands, so it cannot reference itself without already knowing an ID that only exists after that same hash is computed. See Threat model for the full proof.