Specification · v0.3
How a round turns community activity and late-arriving randomness into traits that anyone can recompute. The derivation is implemented and tested today. Indexing, commitments and entropy sourcing are specified here and not yet deployed.
A round is one evolution step. Each round binds four inputs, and every input is fixed before the next one exists:
window(epoch, rule_version) // published when the window opens
→ snapshot_root // committed on-chain after the window closes
→ entropy // requested only after the commitment lands
→ traits = derive(epoch, identity, snapshot_root, rule_version, entropy)Because the snapshot is committed before any entropy exists, and entropy cannot be chosen by any single party, the outcome is unknowable in advance and impossible to steer afterwards. Rendering and narration happen last and only depict the derived traits.
A window is a fixed period (proposed: 7 days) with a rule version announced when it opens. During the window the indexer turns finalized on-chain and community activity into per-identity signals.
| Signal | Source | Cap per identity | Notes |
|---|---|---|---|
| holding_continuity | time-weighted balance presence | 1.0 | √-scaled; resets are not penalised beyond the window |
| participation | verified quest completions | 1.0 | attested by the quest service; deduplicated |
| creativity | curated art/lore submissions | 1.0 | moderated; one credit per submission per window |
| gathering | event attendance attestations | 0.5 | signed attendance; no per-message counting |
contribution(identity) = min(C, Σ_s k_s · √(x_s)) // C = 1.0, k_s per signal
influences = aggregate(contributions) → { participation, creativity, calm } ∈ [0, 100]Never counted
At closes_at + finality buffer, eligibility is frozen into canonical JSON: UTF-8, keys sorted lexicographically, no insignificant whitespace, numbers as shortest round-trip decimals. Its hash is the snapshot root, and it is written to entropy-registry::commit_snapshot before any entropy is requested.
{"entries":[{"identity":"example:ember","weight":0.158},{"identity":"example:fable","weight":0.52}],"epoch":3,"ruleVersion":"oomi-rules/0.3","window":"epoch 3 · days 15–21"}
// first 2 of 10 entries shown
snapshot_root = SHA-256(canonical_json(full snapshot))
= 236222a128d727cba8b3c6fdbca2287a9b7ca1e26cbee452052efe4718716ff3For large snapshots the root becomes a Merkle root, so any holder can verify their own inclusion without downloading the whole file. Leaves are SHA-256(0x00 ‖ entry), nodes SHA-256(0x01 ‖ left ‖ right), leaves sorted, odd nodes promoted. Try it with the example round:
Snapshot entries (epoch 3)
// claimed entry
{"identity":"example:ember","weight":0.158}
// proof (2 steps)
right eb98c1…13a1
left 40a71b…b966
root 3daaaa4b2142a5c4da360679639199a2744c1a60c4819c4d0946cf8c9d29ef29
Entropy is a mix of two independent sources: a physical QRNG (bytes q) and a drand public beacon round B. The QRNG bytes are committed before the beacon round exists, and the beacon round is fixed by rule from the snapshot's slot, so neither source nor the operator can aim at an outcome.
qc = SHA-256("oomi:qrng-commit:v1" ‖ round_id ‖ q)
B = first drand round with timestamp ≥ time(S) + Δ // Δ = 120 s
entropy = SHA-256("oomi:entropy:v1" ‖ round_id ‖ q ‖ σ_drand(B))
fallback (no reveal by D2):
entropy = SHA-256("oomi:entropy:fallback:v1" ‖ round_id ‖ σ_drand(B))| Moment | Operator knows | QRNG knows | drand knows |
|---|---|---|---|
| Snapshot commit (slot S) | snapshot | nothing | nothing |
| QRNG commit (qc) | q | q | B's timing, not its value |
| Beacon round B published | q, σB | q | σB |
| Reveal | everyone can compute entropy |
What this is not
The seed binds every input with fixed-width or 4-byte length-prefixed fields, so no two different inputs can share an encoding. This is the preimage for the pinned test vector:
000000106f6f6d693a6d75746174696f6e3a7631000000070000000d6f6f6d693a666c61677368697016a0eeb0791b6c92451fd284dd9f599e0a7dbe7f6ebea6e2d2d06c7f74aec1120000000e6f6f6d692d72756c65732f302e3367671a2f53dd910a8b35840edb6a0a1e751ae5532178ca7f025b823eee317992
123 bytes → SHA-256 → 32-byte seed
seed = SHA-256(lp("oomi:mutation:v1") ‖ u32be(epoch) ‖ lp(identity)
‖ snapshot_root[32] ‖ lp(rule_version) ‖ entropy[32])
u_i = u32be(SHA-256(seed ‖ u32be(i))[0..4]) / 2^32 // i = trait index
trait_i = pick(weights_i adjusted by influences, u_i)
lp(x) = u32be(len(utf8(x))) ‖ utf8(x)function pick(options, u) { // options: [{ value, p }], Σp = 1
let acc = 0;
for (const o of options) {
acc += o.p;
if (u < acc) return o.value;
}
return options[options.length - 1].value; // float-rounding guard
}Influences shift option weights multiplicatively by at most ±30% (factor clamped to [0.7, 1.3]). They change probabilities, never outcomes, and can't push any option to 0 or 1.
| Input | Value |
|---|---|
| epoch | 7 |
| identity | oomi:flagship |
| snapshot_root | SHA-256("snapshot") = 16a0eeb0791b6c92451fd284dd9f599e0a7dbe7f6ebea6e2d2d06c7f74aec112 |
| rule_version | oomi-rules/0.3 |
| entropy | SHA-256("entropy") = 67671a2f53dd910a8b35840edb6a0a1e751ae5532178ca7f025b823eee317992 |
| → seed | 0c583ec8ee6228344fb07a2fdd0508d70062a5c58289826043a96f2999b2f908 |
Base weights per trait. Trait index i follows the order below. Changing any weight or order requires a new rule version, published before the window that uses it.
Aurai = 0
Ear glowi = 1
Dropletsi = 2
Patterni = 3
Temperamenti = 4
| Influence | Trait | Favours (+) / disfavours (−) |
|---|---|---|
| participation | Ear glow | +Bright, +Radiant, −Dim |
| participation | Droplets | +Orbiting, +Swarming, −Still |
| creativity | Pattern | +Crystalline, +Rippled, −Marbled |
| creativity | Aura | +Dusk Violet, +Tidal Teal, −Signal Cyan |
| creativity | Temperament | +Playful, +Mischievous |
| calm | Temperament | +Serene, +Focused, −Mischievous |
| calm | Aura | +Deep Plasma, +Halo Gold |
| calm | Droplets | +Still, −Swarming |
| Transition | Instruction | Guard |
|---|---|---|
| Scheduled → Sealed | commit_snapshot | now ≥ closes_at + buffer; one-shot per epoch |
| Sealed → QrngCommitted | commit_qrng | snapshot committed in an earlier, finalized slot |
| QrngCommitted → Revealed | reveal | H(q) = qc; B = rule(S); σB verified off-chain and stored |
| QrngCommitted → Fallback | fallback | deadline D2 passed with no reveal (anyone may call) |
| Revealed | Fallback → Derived | commit_outcomes | outcomes root of the pure derivation |
| Derived → Rendered | (off-chain) | art and narration approved by a reviewer |
| Failure | Effect | Handling |
|---|---|---|
| QRNG provider down | Round delayed | Fixed fallback to beacon-only entropy after D2 |
| Operator withholds reveal | Round delayed | Anyone can trigger the fallback; no alternative outcome exists |
| Chain reorg near the cutoff | Snapshot inputs uncertain | Finalized slots only, plus a buffer before sealing |
| Indexer bug | Wrong signals | Raw data retained; annotated re-snapshot under a new round id, never a silent edit |
| Rendering fails | No art yet | Outcome unaffected; art follows later |
| Rule change mid-window | Not allowed | publish_rule_version is rejected while a window is open |
$OOMI balances stay fungible. A holder may opt in to link an identity record (evolution-registry::link_identity), which then gets its own derivation per round using its identity string in the seed. Linking is reversible (rent is refunded on unlink), viewing never requires a transaction, and only hashes, never art or personal data, are stored on-chain.
After derivation, the render pipeline turns traits into art direction and the agent drafts narration for human review. Neither can write to the round record, and a failed or rejected render leaves the outcome unchanged. The AI depicts outcomes; it never chooses them.