# WAI Extension: Live Moderation > Status: Draft. Registers the record `wai.live.moderation`: moderation for rooms. The room > authority signs a **review policy**, `WMP1`: which event kinds and media are held for review > before delivery or reviewed after it, by whom, the deadline of each, the fallback when no > decision arrives in time (allow, deny or hold), and the reason codes in use, each with its > ground. A moderator's verdict is a `decision` event of [live-events](live-events.md) (§4.11 > there), unchanged, followed by a `meta` event whose payload is this extension's > **statement**, `WMS1`: what kind of thing the decision targets (an event, or a media object > such as a recorded chunk), the decision it supersedes, its basis (own initiative, a notice, > a complaint, or the policy's fallback), whether automated means detected the content or took > the decision, and its ground, explanation and facts. Decisions on one target form a line by > digest, so a later decision overrides an earlier one and the outcome of an appeal is a > decision too. Anyone can submit a signed **notice**, `WMN1`, about an event or an object, and > anyone affected a **complaint** about a decision. A restricted item is withheld, and where it > is withheld from a chain, a recording or a clip, its absence is proven by the decisions > that restrict it. Each restricting decision maps, deterministically, onto one record of the > statement-of-reasons format of the database the European Commission keeps under Regulation > (EU) 2022/2065. No new transport. The reference implementation is the `live_moderation` > module of `wai-rs` (feature `live_moderation`); the corpus is > [`live-conformance/moderation/`](../live-conformance/moderation/), and its second > implementation is `live-conformance/live_moderation_verify.py`. > Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. > > Texts read for this revision, each pinned to the version named: > Regulation (EU) 2022/2065 (OJ L 277, 27.10.2022, p. 1), Articles 16, 17, 20 and 24(5), as the > Publications Office serves its only consolidated version, 02022R2065-20221027, with no > amending act recorded and no English corrigendum; the DSA Transparency Database's "API and > schema" documentation and its explanation of the statement attributes > (`transparency.dsa.ec.europa.eu`), for the submission schema in force from 1 July 2025, which > the documentation's changelog calls API v2 and which is still submitted at `/api/v1/statement`, > as the database's published source has it at revision `fc3b1fd5` (2026-09-23) of the > documentation and revision `bf0afeb8` (2026-06-09) of the submission's validation rules; and > Commission Implementing Regulation (EU) 2024/2835, Annex II, whose categories the database > uses. An article or paragraph number in this text is the Regulation's unless it says > otherwise. ## 1. Scope In scope: the review policy, its signature, its rules and the policy in effect at each instant (§3); the decision event as this extension reads it, the statement, who may decide, lines of decisions and fallback decisions (§4); notices and complaints (§5); the items under review, their states, and what a relay and a sink do with them (§6); verification, with the gaps that restricted items leave explained (§7); the effect of a restriction on a stream, a recording and a clip (§8); and the statement-of-reasons export (§9). Out of scope: a transport; how a moderator or a classifier reaches a verdict; how a notice or a complaint reaches the room authority; the notification of a submitter or an affected participant (Article 16(5), Article 17(1), Article 20(5)); the submission of a record to the database, which is an authenticated HTTP request this extension does not define; and the restriction of accounts, payments or the provision of a service (Article 17(1)(b) to (d)), which no WAI record expresses. This extension states what the records carry and how they map onto the database's format. Which decisions a service takes, and which records it submits, is not its subject. Relationship to the core spec: `wai.live.moderation` is a **record** ([SPEC](../SPEC.md) §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable. Relationship to [live-events](live-events.md): this extension changes nothing in it. Its `decision` payload (§4.11 there) holds a verdict, a target digest and a reason code and no more, so the statement rides in the `meta` kind, which live-events leaves to applications (§4.2 there), and is bound to its decision by the event chain (§4.2 here), as [timed-text](timed-text.md)'s annotation is bound to its caption. A decision's target is the digest of an event, as live-events has it, or, where its statement says so, of a media object (§4.1). Relationship to [live-rooms](live-rooms.md): the room descriptor's `review` field (§3.6 there) fixes the room's review at its creation; a review policy refines it and never contradicts it (§3.5). The moderate power (§3.3 there) is the power to decide. Relationship to [live-recording](live-recording.md): a restriction changes what a recording package holds and what a clip presents (§8). A clip's inclusion proof does not change: it is over the chunks' descriptors, which a restriction leaves in place. ## 2. Terms - **Room**, **room clock**, **principal**, **event**, **chain**, **grant**, **room key**, **merged order**: as in live-events §2 and live-rooms §2 and §9.2. A room key is the room authority's key or the clock's. - **Item**: an event of a decidable kind (§4.1), or a media object (§4.1), that a policy reviews or a decision targets. - **Decision**: a `decision` event (live-events §4.11). Its **statement** is the `WMS1` payload of the `meta` event that follows it in its chain (§4.2). - **Line** of a target: the decisions on it, the first superseding none and each later one superseding the one before (§4.5). Its last decision is the **standing** decision. - **Restricting** verdict: deny or redact. An item whose standing decision restricts it is **restricted**. - **Withheld**: not delivered to a subscriber session, not presented by a sink, or, for redaction, not kept in a recording package (§8). - **Reviewer**: a principal a policy names to decide on the items it reviews (§3.1). - **Moderator**: a principal whose grant in effect has the moderate power (live-rooms §3.3), or a room key. Integers are little-endian. A digest is SHA-256 (FIPS 180-4), 32 bytes. Ed25519 is RFC 8032's, verified strictly (SPEC §7 *Signatures*): a key that fails step 1 or 2 is `bad_key`, and a signature that fails a later step is `bad_signature`. A *text* field is a u16 length and that many bytes, which are UTF-8 (RFC 3629): no overlong form, no surrogate, nothing above U+10FFFF. ## 3. The review policy ### 3.1 Layout | field | size | meaning | |---|---|---| | `magic` | 4 | `WMP1` (ASCII) | | `version` | 1 | 1 in this revision | | `room` | 32 | the room id (live-rooms §3.2) | | `prev` | 32 | the digest of the policy this one replaces (§3.4); 32 zero bytes for the first | | `effective` | 8 | u64: the room-clock instant from which this policy applies; 0 for the first | | `reviewers` | 1 + 32 × *n* | a u8 count (0 to 16), then each reviewer's Ed25519 key | | `flaggers` | 1 + 32 × *n* | a u8 count (0 to 16), then each key the authority recognises as a trusted flagger's (§9.3 `source_type`) | | `entries` | 1 + 10 × *n* | a u8 count (0 to 32), then each entry: `scope` (u8), `code` (u8), `mode` (u8), `deadline_ms` (u32), `fallback` (u8), `fallback_reason` (u16) | | `reasons` | 1 + … | a u8 count (1 to 64), then each reason code: `code` (u16), `ground` (u8), `category` (u8), `reference` (text), `url` (text) | | `territory` | 1 + 2 × *n* | a u8 count (1 to 30), then each state's code (§9.3 `territorial_scope`), two ASCII capitals | | `redress` | text | how an affected participant can seek redress (§9.4, Article 17(3)(f)) | | `sig` | 64 | §3.2 | An **entry** says how items of one kind are reviewed: - `scope` 1: `code` is an event kind (live-events §4), and it must be a decidable kind (§4.1); `scope` 2: `code` is a media kind (live-rooms §6.1: 1 audio, 2 video, 3 screen), and the entry reviews every media object of that kind. - `mode` 1 **pre**: an item is held from delivery until it is decided or its deadline passes; `mode` 2 **post**: an item is delivered at once and reviewed after. - `deadline_ms`: how long after the item's `t` a decision is awaited, in milliseconds of room clock, above 0. - `fallback`, what applies at the deadline when no decision has come: 1 **allow**, 2 **deny**, 3 **hold** (the item stays withheld until a decision comes). - `fallback_reason`: the reason code a fallback that denies or holds applies under; 0 when `fallback` is allow. A **reason code** is one entry of the room's vocabulary: - `code`: the value a decision's `reason` (live-events §4.11) carries, above 0; - `ground`: 1 **illegal content**, 2 **incompatible with the terms** of the service; - `category`: the database category it maps to, by its number in §9.5; - `reference`: the legal provision, or the clause of the terms, the code rests on: 1 to 500 code points; - `url`: empty, or an `https://` URL where the terms or the provision can be read: `https://` then bytes of `!` to `~` (0x21–0x7E), at most 500 bytes in all. ### 3.2 Digest and signature The **digest** of a policy is SHA-256 over its record from `magic` through `redress`. The **signature** is Ed25519 by the room descriptor's `authority` over the 27 bytes `wai:live-moderation-policy` (ASCII) and 0x01, followed by the bytes the digest covers. ### 3.3 Reading and rules A reader reads exactly the bytes it is given as one policy, in the order of §3.1, and refuses at the first of these that holds: 1. Fewer than 4 bytes: `truncated`. A magic other than `WMP1`: `malformed`. No byte after the magic: `truncated`. A version other than 1: `unknown_version`. 2. The bytes end before a field the layout requires: `truncated`. 3. Bytes after `sig`: `malformed`. It then holds the policy to these rules, and refuses one that breaks any of them as `policy_malformed`: - a count is within its range; - the reviewers are in strictly ascending byte order, and so are the flaggers; - the entries are in strictly ascending order of (`scope`, `code`), with `scope` 1 or 2, `code` as `scope` requires, `mode` 1 or 2, `deadline_ms` above 0, `fallback` 1, 2 or 3, and `fallback_reason` 0 when `fallback` is 1 and a code of `reasons` otherwise; - the reason codes are in strictly ascending order of `code`, each above 0, with `ground` 1 or 2, `category` 1 to 16, and `reference` and `url` as §3.1 has them; - the territory's codes are in strictly ascending order, each one of the thirty of §9.3; - every text field is UTF-8. Then, in the steps of §7.2: its `room` is the room id (`wrong_room`); its signature verifies (`bad_key`, `bad_signature`); its link (§3.4, `policy_unlinked`); and the room's review field (§3.5, `policy_conflicts_room`). ### 3.4 The policy in effect A verifier is given a room's policies in a list. The first has `prev` zero and `effective` 0. Each later one has as `prev` the digest of the one before it in the list, and an `effective` above that one's. A policy that breaks this is refused `policy_unlinked`. The **policy in effect** at room-clock instant *t* is the last of the list whose `effective` is at or below *t*. A room given no policy has none in effect at any instant. A policy is replaced, never amended: the authority signs a new one, chained to the old, with the instant it takes effect. Each item is reviewed under the policy in effect at its `t`, and each decision is checked against the policy in effect at the decision's `t`. ### 3.5 The room's review field The room descriptor's `review` field (live-rooms §3.6) names the event kinds held for review, one deadline and one fallback, allow or deny; the room id commits to it, so every principal that joined the room joined under it. Every policy of the room **MUST** keep it: for each kind that field names, the policy has an entry of `scope` 1 for that kind, with `mode` 1 (pre), the field's `deadline_ms` and the field's `fallback`. Otherwise `policy_conflicts_room`. A policy may add entries for other kinds and for media, and name reviewers, flaggers and the vocabulary, which the field does not hold. ## 4. Decisions ### 4.1 The decision event A decision is a `decision` event (live-events §4.11): `verdict` (1 allow, 2 deny, 3 redact), `target` (32 bytes) and `reason` (u16). Its statement (§4.2) says what `target` names: - an **event**: the digest of an event of the room (live-events §3.2), of a **decidable kind**: `meta` (other than a statement), `cue`, `chat`, `reaction`, `poll_open`, `vote`, `poll_close`, `marker` or `caption`. A `control`, a `decision`, a `take_commit` and a lifecycle event are not decided on: an appeal of a decision is a decision that supersedes it (§4.5), not one that targets it; - a **media object**: its digest. A chunk of a take is named by its leaf ([capture-binding](capture-binding.md) §4.2), which binds its index, its capture range, its length and the digest of its bytes, so the name is the chunk's in its take; any other object, by the digest its receipt names it with ([jwp-receipts](jwp-receipts.md) §2.1). A decision without a statement targets an event, supersedes nothing, and is of the moderator's own initiative. Verdicts: - **allow**: the item is shown; - **deny**: access to the item is disabled: it is withheld from delivery and presentation, and a recording keeps it, so that a later decision can restore it (§8); - **redact**: the item is removed: it is withheld as for deny, and a recording does not keep it (§8.2). A restriction lasts until a later decision of its line supersedes it. A restriction for a set time is a decision and, at the time's end, an allow that supersedes it. ### 4.2 The statement A decision's **statement** is the payload of a `meta` event whose first four bytes are `WMS1`, by the same principal, at the decision's `seq` + 1 and at the decision's `t`. | offset | size | field | meaning | |---|---|---|---| | 0 | 4 | `magic` | `WMS1` (ASCII) | | 4 | 1 | `version` | 1 in this revision | | 5 | 1 | `target_type` | 1 an event, 2 a media object (§4.1) | | 6 | 32 | `target_prev` | for an event: the target's `prev` (live-events §3.1), so that a chain withholding it stays linked (§7.3); for an object: 32 zero bytes | | 38 | 32 | `supersedes` | the digest of the decision event this one supersedes (§4.5); 32 zero bytes for none | | 70 | 1 | `basis` | 1 the moderator's own initiative, 2 a notice, 3 a complaint, 4 the policy's fallback (§4.6) | | 71 | 32 | `notice` | for basis 2 and 3: the digest of the notice or complaint it answers (§5); otherwise 32 zero bytes | | 103 | 1 | `automated` | bit 0: the content was detected or identified by automated means; bits 1 and 2: how far the decision was taken by automated means, 0 not, 1 partially, 2 fully; every other bit 0 | | 104 | 1 | `ground` | 1 illegal content, 2 incompatible with the terms | | 105 | text | `ground_ref` | the legal provision or the clause of the terms relied on; empty when the reason code's `reference` is the one | | … | text | `explanation` | why the item is considered illegal, or incompatible, on that ground | | … | text | `facts` | the facts and circumstances relied on in taking the decision | ### 4.3 Reading a statement A statement is refused `statement_malformed` when: it ends before a field or holds bytes after `facts`; its version is not 1; `target_type` is not 1 or 2; `target_type` is 2 and `target_prev` is not zero; `basis` is not 1 to 4; `notice` is zero for basis 2 or 3, or not zero for basis 1 or 4; `automated` sets a bit above bit 2, or bits 1 and 2 read 3; `basis` is 4 and `automated`'s bits 1 and 2 do not read 2, or `supersedes` is not zero; `ground` is not 1 or 2; or a text field is not UTF-8. The `meta` payload is bounded by the principal's policy (live-events §5, `max_payload`), 1024 bytes by default, which bounds the three texts together. ### 4.4 Who decides A decision is the event of a moderator. The room verifier already holds every decision to it: a grant whose policy permits `decision` has the moderate power (live-rooms §3.3, §4.3 step 5), and a participant's event is verified under its grant in effect at its `t` (live-rooms §9.2 step 4). A decision by a principal without the moderate power is therefore refused by the room, `kind_not_permitted`, before this extension reads it. A room key decides as the authority, without a grant. Where the target is an item a policy reviews (§6.1) and the policy in effect at the decision's `t` names reviewers, the decider is one of them or a room key: otherwise `not_reviewer`. ### 4.5 Lines: superseding by digest The decisions on one target form one line. The first supersedes none; each later decision names in `supersedes` the line's last decision before it, so that it overrides it from its own `t`. A verifier checks each decision, in the merged order, against the line as it stands: - `supersedes` zero while the target already has a decision: `supersede_gap`; - `supersedes` naming a decision the verifier does not hold: `supersede_unknown`; - naming a decision on another target: `supersede_mismatch`; - naming a decision of this target that is not the line's last (one another decision already supersedes): `supersede_gap`. An **appeal** is a complaint (§5) against a decision, and its outcome is a decision of basis 3 that supersedes the decision complained of and names the complaint. It may reverse that decision (allow after deny) or confirm it. ### 4.6 Fallback decisions At an item's deadline with no decision, its entry's fallback applies (§6.2) whether or not any record says so. A room key **SHOULD** sign it, as a **fallback decision** at or after the deadline: basis 4, `automated` fully, `supersedes` zero, the entry's fallback as its verdict (1 allow, 2 deny) and the entry's `fallback_reason` as its `reason`. A fallback decision lets a subscriber that never received the item explain its absence (§7.3), and gives the restriction a signed record to export (§9). A fallback of hold has no decision: the item waits. A fallback decision is refused `fallback_not_in_policy` when: its author is not a room key; its target is not an item reviewed under an entry whose fallback is allow or deny; or its verdict is not that fallback, or its `reason` not the entry's `fallback_reason`. It is refused `fallback_early` when its `t` is before the item's deadline. It supersedes nothing (§4.3), so a target that already has a decision refuses it by its line (§4.5, `supersede_gap`): a decision by the deadline forestalls the fallback. ## 5. Notices and complaints ### 5.1 Layout A **notice** (Article 16) says that an item is illegal content, or incompatible with the terms; a **complaint** (Article 20) contests a decision. Anyone can sign one with a key of their own; neither needs a grant of the room. | field | size | meaning | |---|---|---| | `magic` | 4 | `WMN1` (ASCII) | | `version` | 1 | 1 in this revision | | `room` | 32 | the room id | | `type` | 1 | 1 a notice, 2 a complaint | | `target_type` | 1 | a notice: 1 an event, 2 a media object (§4.1); a complaint: 1, the decision event complained of | | `target` | 32 | the target's digest: the exact location of the information (Article 16(2)(b)) | | `t` | 8 | u64: the room-clock instant at which it was submitted | | `ground` | 1 | a notice: 1 illegal content, 2 incompatible with the terms; a complaint: 0 | | `good_faith` | 1 | 1: the submitter confirms its bona fide belief that what it states is accurate and complete (Article 16(2)(d)); 0: it does not | | `ground_ref` | text | the provision or clause the submitter cites; may be empty | | `statement` | text | the submitter's explanation of its reasons (Article 16(2)(a)), or of its complaint | | `contact` | text | the submitter's name and email address (Article 16(2)(c)); may be empty | | `submitter` | 32 | the Ed25519 key that signs it | | `sig` | 64 | §5.2 | ### 5.2 Digest, signature and rules The **digest** is SHA-256 over the record from `magic` through `submitter`. The **signature** is Ed25519 by `submitter` over the 27 bytes `wai:live-moderation-notice` (ASCII) and 0x01, followed by the bytes the digest covers. A reader reads it as §3.3 steps 1 to 3 read a policy, with `WMN1` for `WMP1`, and refuses as `notice_malformed` one in which: `type` is not 1 or 2; a notice's `target_type` is not 1 or 2, or its `ground` not 1 or 2; a complaint's `target_type` is not 1 or its `ground` not 0; `good_faith` is not 0 or 1; a text field is not UTF-8; or whose digest is an earlier notice's in the list. Then its `room` is the room id (`wrong_room`), and its signature verifies (`bad_key`, `bad_signature`). A decision of basis 2 names a notice (`type` 1) and one of basis 3 a complaint (`type` 2) that the verifier is given, otherwise `notice_unknown`. A notice's `target` is the decision's target, and its `target_type` the statement's; a complaint's `target` is the decision the answering decision supersedes; and its `t` is at or before the decision's `t`. Otherwise `notice_mismatch`. A complaint may be submitted by anyone the decision affects, the participant whose item it restricts or the submitter of the notice it answers among them; this extension does not check who. `contact` is personal data. No export carries it (§9.7). ## 6. Review ### 6.1 Items An **item under review** is: - an event the verifier holds, of a decidable kind (§4.1), whose kind an entry of `scope` 1 of the policy in effect at its `t` names, and whose author is neither a room key nor a principal whose grant in effect at its `t` has the moderate power; or - a media object the verifier is given (§7.1), whose media kind an entry of `scope` 2 of the policy in effect at its `t` names. Its entry gives its `mode`, its **deadline**, `t` + `deadline_ms` × 1000, and its fallback. An item that is not under review but that a decision targets is an item too, with no entry. ### 6.2 States An item's **state** at room-clock instant *x*, from its `t` on, is: 1. when a decision on it has its `t` at or below *x*: the verdict of the last such decision of its line, **allowed**, **denied** or **redacted**; 2. otherwise, an item with no entry: **shown**; 3. otherwise, before its deadline: **held** in mode pre, **pending** in mode post; 4. otherwise (at or after the deadline, with no decision): **fallback_allow**, **fallback_deny** or **fallback_hold**, by its entry's fallback. The fallback applies at the deadline unless a decision other than a fallback decision (§4.6) has its `t` at or below the deadline; a fallback decision records the fallback, and does not forestall it. An item is **visible** in the states shown, pending, allowed and fallback_allow, and withheld in every other. A decision decides an item from its own `t` whatever came before it, so a moderator's decision after a fallback overrides the fallback, and an appeal's outcome overrides the decision it supersedes. A moderator's decision at the deadline itself is a decision by the deadline, and no fallback applies. An item's **timeline** is the sequence of its states from its `t`: one entry at its `t`, one at its deadline when the fallback applies, and one at each decision on it in the merged order. The entries are in the order of their instants and, at one instant, in that order (the item's, the fallback's, the decisions'). Each entry is the instant, the state, the decision's digest where a decision made it, and whether the state is visible. The item's state at an instant is that of its last entry at or before it. ### 6.3 What a relay does A relay that delivers a room's tracks under a policy applies it to each subscriber session: - An item in state **held** is delivered at once to the reviewers (or, with none named, to every present principal whose grant in effect has the moderate power) and to the room keys, and is withheld from every other subscriber session. The relay keeps it until its state becomes visible, when it delivers it to the sessions it withheld it from, or until a decision denies or redacts it. The deadline is the longest it waits for a decision before the fallback applies: at the deadline, under allow it delivers the item; under deny it withholds it for good unless a later decision allows it; under hold it keeps it until a decision comes, for as long as the session lasts. - An item in state **pending** is delivered at once. At the deadline with no decision, under deny or hold the relay stops delivering it, to a session that joins later or fetches it again. - A restricted item is withheld from every session but the reviewers' and the room keys', and a relay **MUST NOT** deliver an event or object in a withheld state to any other session, live, in a join set (live-events §8.2) or in answer to a FETCH. - A relay that withholds an event goes on delivering the events after it in its chain, so a session sees a gap (live-events §6.2). When the event is delivered later, the session fills the gap as live-events §6.2 has a receiver fill one: by verifying the chain again from its start. A withheld event that stays restricted leaves a gap, which the decisions that restrict it explain (§7.3). - An event a relay withholds from a session is not delivered to it, and its per-hop record ([live-metering](live-metering.md) §3.3) does not count it as delivered there. A relay that signs fallback decisions (§4.6) does so as the room's clock. ### 6.4 What a sink shows A sink shows an item at instant *x* only when its state at *x* is visible, by the decisions it holds. Holding a decision's event and statement, it **SHOULD NOT** show what the decision restricts (live-events §4.11). A sink that holds a decision but not its target shows nothing of the target, and states the decision's verdict and reason in its place where it shows the chain. A sink that labels a restriction for a viewer takes the label from the statement and the reason code's vocabulary entry, never from the target's content. ## 7. Verification ### 7.1 Inputs A moderation verifier is given what a room verifier is given (live-rooms §9.1: a descriptor, grants, a key-log head or none, and the chains), and: - **policies**: the room's review policies, in a list (§3.4); - **notices**: notices and complaints, in a list (§5); - **objects**: the media objects under moderation, each its digest, its media kind (1 audio, 2 video, 3 screen) and its `t`, the room-clock instant it reached the relay. For a take's chunks, the digest is the chunk's leaf and the `t` that of the commitment that adds it (capture-binding §5). The objects are the caller's statement, as the policies a live-events verifier is given are; no two have one digest. ### 7.2 Steps It runs these steps and stops at the first refusal, which it reports with the reason and where: 1. **The room** (live-rooms §9): a refusal is the moderation verifier's, at `room` with the room verifier's own place. A gap is not a refusal: the steps go on, and §7.3 says which gaps decisions explain. 2. **The policies**, in the list's order (§3.3, §3.4, §3.5): the refusal, at the policy's index. 3. **The notices**, in the list's order (§5.2): the refusal, at the notice's index. 4. **The statements.** For each `meta` event the room accepted whose payload begins with `WMS1`, in the merged order: when its `seq` is 0, `statement_misplaced`; when the verifier holds its principal's event at `seq` − 1, that is a decision at the same `t`, otherwise `statement_misplaced`; when the verifier does not, the statement is an orphan of a delivery gap, and is read no further. A statement in place reads (§4.3), otherwise `statement_malformed`. At the statement's record. 5. **The gaps** (§7.3): which gaps restricted items explain. No refusal. 6. **The decisions**, in the merged order, each by §7.4: the refusal, at the decision's record. 7. **The outcome** (§7.5). A **gap remains** when the room has a gap that §7.3 does not explain. With a gap remaining, a check of §7.4 that fails because the verifier does not hold a decision, an event or a statement is **unresolved**, which is not a refusal; with none, it is the refusal the check names. ### 7.3 Explained gaps A gap of principal *P*, seqs *a* to *b* (live-events §6.2), is **explained** when the verifier holds *P*'s event at *b* + 1, and walking back from it reaches *P*'s event at *a* − 1 (or the start of the chain, when *a* is 0) through restricted events only: 1. *D* is the `prev` of the event at *b* + 1. 2. For each *s* from *b* down to *a*: of the decisions the room accepted whose target is *D*, the last in the merged order has a statement of `target_type` 1 and a restricting verdict, otherwise the gap is not explained; seq *s* is withheld by that decision, and *D* becomes its statement's `target_prev`. 3. *D* is then the digest of the event at *a* − 1, or 32 zero bytes when *a* is 0; otherwise the gap is not explained. Each step is checked from what the verifier holds: the link from the gap to the event after it is the event's own `prev`, and each link inside the gap is a moderator's signed statement about an event it decided on, so no event is passed off as withheld that its chain does not hold at that place. An explained gap is not a delivery gap. A gap that is not explained stays one, with live-events' code `delivery_gap`. The events an explained gap withholds are **withheld targets**: the verifier knows their digests and their places, not their kinds or their times. ### 7.4 Each decision For a decision *E* of principal *P*, with target *X*, in the merged order, its statement being *P*'s event at `seq` + 1 when that is a statement in place (step 4), **unknown** when the verifier does not hold that seq because a gap remains there, and **none** otherwise: 1. **The reason.** A policy is in effect at *E*'s `t`, its vocabulary has *E*'s `reason`, and a known statement's `ground` is that code's: otherwise `reason_not_in_policy`. 2. **The target.** - Of type 2: an object the verifier is given has digest *X* and a `t` at or below *E*'s: otherwise `target_unknown`. - Of type 1 (or with no statement): when *X* is the digest of an event the room accepted, that event comes before *E* in the merged order, otherwise `target_unknown`; it is of a decidable kind, otherwise `target_not_decidable`; and a known statement's `target_prev` is its `prev`, otherwise `target_prev_mismatch`. When *X* is a withheld target (§7.3), these hold. Otherwise `target_unknown`, unresolved while a gap remains. 3. **The reviewer** (§4.4): `not_reviewer`. Not made for a withheld target. 4. **The line** (§4.5): `supersede_gap`, `supersede_unknown` (unresolved while a gap remains) or `supersede_mismatch`. Not made for an unknown statement; with none, `supersedes` is zero. 5. **The basis**, for a known statement: of basis 2 or 3, §5.2 (`notice_unknown`, `notice_mismatch`); of basis 4, §4.6 (`fallback_not_in_policy`, `fallback_early`), its entry's checks not made for a withheld target. *E* then becomes the last decision of *X*'s line. ### 7.5 Outcomes and what a verification states - **refuse**: the reason and where (§7.2). - **gap**: no refusal, and a gap remains: the gaps that remain, under the code `delivery_gap`, and what **accept** states, computed from what the verifier holds. - **accept**: no refusal and no gap remaining. With accept or gap, the verifier states: - the room's own outcome (live-rooms §9.4), accept or gap; - each explained gap: its principal, its seqs, and the decision withholding each seq; - each decision, in the merged order: its digest, its decider, its `t`, its target and target type, its verdict, reason and basis (none for an unknown statement), what it supersedes, the notice it answers, whether its statement is known, none or unknown, and whether it stands at the end; - each item, events in the merged order and then objects in the order given: its digest, its kind or media, its `t`, its entry (mode, deadline, fallback) or none, and its timeline (§6.2); - each decision's export (§9): the record, none for an allow, or the refusal. ## 8. Redaction in streams, recordings and clips A restricted item is **withheld**, never replaced, and its absence is **proven**: whatever held it shows where it was, and the decision that restricts it shows why. A WAI chain, a take tree and a clip all bind their contents by digest, so an item cannot be swapped for a stand-in without breaking them; withholding keeps them whole, because each of them names the item without holding it. ### 8.1 Streams An event withheld from a subscriber session leaves a gap in its author's chain, and the decisions that restrict it explain the gap (§7.3): the session knows the event existed, where, under which decision and why, and holds nothing of it. A media object withheld from a session is an object the relay does not forward. A restricted object's receipts, which name it by its digest, stay where they are. ### 8.2 A recording package A recording sealed after a decision applies it to what it keeps: - A **redacted** chunk's file is left out of its take component, and a redacted event's record out of its principal's chain file. The take's manifest still lists the chunk's descriptor, and the next event of the chain still names the redacted one in its `prev`. - A **denied** item is kept, so that a later decision can allow it; every presentation of the recording (a clip, a playlist, a sink) withholds it. The recording verifier ([live-recording](live-recording.md) §4.4) then finds the take's chunk missing and the chain's gap, and fails `complete` with `chunk_missing` or `delivery_gap`. A moderation verifier explains them: a chunk the manifest lists, with no file, whose leaf a standing redaction names, and a gap §7.3 explains with redactions alone. Where every missing chunk and every gap is so explained, the recording is complete but for what was redacted. What this revision implements of this section: nothing beyond §7.3 and §8.3. The reference explains gaps in a room's chains and verifies clips; it does not verify a package under moderation, and the corpus has no package vector. ### 8.3 Clips A clip's chunks are proved against a commitment the device made while it captured, by a range proof over their descriptors (live-recording §5.3). A restriction leaves the descriptors in place, so the proof still verifies over every chunk the clip covers; what changes is which chunks' bytes the clip presents. A chunk is **restricted** when its leaf is the target of a standing decision that restricts it. A **clip under moderation** is verified by live-recording §5.4 with two changes: 1. In step (k), a restricted chunk is passed over: its bytes are neither required nor checked, and bytes given for it are not presented. The part presents the bytes of its other chunks, in order, less `trim_in` when its first chunk is not restricted and `trim_out` when its last is not. 2. Then, after every part: a restricted chunk whose bytes are given is `redaction_not_applied`, at the first such chunk. A clip that passes is accepted, and states what live-recording §5.4 states, and for each part the indexes of its restricted chunks, each with the decision that restricts it. A clip whose restricted chunk is restored by a later allow presents it again, and is refused `clip_chunk_missing` when its bytes are not given. A clip's inclusion proof thus verifies in every case, and a clip fails only with a stated reason: one of live-recording §9's, or `redaction_not_applied`. ## 9. The statement-of-reasons export ### 9.1 The format and its version A restricting decision maps onto one **statement of reasons** in the form the DSA Transparency Database accepts at its single-statement endpoint (`POST /api/v1/statement`): the submission schema in force from 1 July 2025, which the database's documentation calls API v2, read at the revisions named at the head of this text. The export is the request body. An allow restricts nothing, and has no export. ### 9.2 The record The record is one JSON object (RFC 8259) whose members are those of §9.3 that apply, in the order of §9.3, written with no whitespace. Each string is UTF-8, written with `"` as `\"`, `\` as `\\`, U+0008, U+0009, U+000A, U+000C and U+000D as `\b`, `\t`, `\n`, `\f` and `\r`, any other code point below U+0020 as `\u` and four lowercase hexadecimal digits, and every other code point as itself. There is no byte after the closing brace. A date is `YYYY-MM-DD`, the UTC date of the instant `t0` + `t` of the room clock, `t0` the descriptor's. ### 9.3 Members and their sources "Closed" marks a member whose value is one of a list the database fixes; "free" a text the database accepts up to the length given, in characters (code points). | member | value | source | form | |---|---|---|---| | `decision_visibility` | `["DECISION_VISIBILITY_CONTENT_DISABLED"]` for deny, `["DECISION_VISIBILITY_CONTENT_REMOVED"]` for redact | the decision's `verdict` | closed, an array | | `decision_facts` | the statement's `facts` | statement | free, 1 to 5000 | | `decision_ground` | `DECISION_GROUND_ILLEGAL_CONTENT` (1) or `DECISION_GROUND_INCOMPATIBLE_CONTENT` (2) | statement `ground` | closed | | `decision_ground_reference_url` | the reason code's `url`, only when it is not empty | policy | a URL, up to 500 | | `illegal_content_legal_ground` | ground 1 only: the statement's `ground_ref`, or the reason code's `reference` when that is empty | statement, policy | free, 1 to 500 | | `illegal_content_explanation` | ground 1 only: the statement's `explanation` | statement | free, 1 to 2000 | | `incompatible_content_ground` | ground 2 only: as `illegal_content_legal_ground` | statement, policy | free, 1 to 500 | | `incompatible_content_explanation` | ground 2 only: as `illegal_content_explanation` | statement | free, 1 to 2000 | | `content_type` | a one-element array, by the target (§9.6) | the target's kind or media | closed, an array | | `content_type_other` | only with `CONTENT_TYPE_OTHER`: §9.6's text | the target's kind | free, up to 500 | | `category` | the reason code's `category`, by §9.5 | policy | closed | | `territorial_scope` | the policy's `territory`, in its order | policy | closed, an array of the thirty codes `AT`, `BE`, `BG`, `CY`, `CZ`, `DE`, `DK`, `EE`, `ES`, `FI`, `FR`, `GR`, `HR`, `HU`, `IE`, `IS`, `IT`, `LI`, `LT`, `LU`, `LV`, `MT`, `NL`, `NO`, `PL`, `PT`, `RO`, `SE`, `SI`, `SK` | | `content_date` | the date of the target's `t` (an event's, or an object's as given), from 2000-01-01 to 2038-01-01 | the target | a date | | `application_date` | the date of the decision's `t`, from 2020-01-01 to 2038-01-01 | the decision | a date | | `source_type` | basis 1 and 4: `SOURCE_VOLUNTARY`; basis 2: `SOURCE_TRUSTED_FLAGGER` when the notice's ground is 1 and its submitter is one of the policy's `flaggers`, `SOURCE_ARTICLE_16` when its ground is 1 and it is not, `SOURCE_TYPE_OTHER_NOTIFICATION` when its ground is 2; basis 3: the `source_type` of the decision it supersedes | statement, notice, policy | closed | | `automated_detection` | `Yes` when `automated` bit 0 is set, `No` otherwise | statement | closed | | `automated_decision` | `AUTOMATED_DECISION_NOT_AUTOMATED` (0), `AUTOMATED_DECISION_PARTIALLY` (1) or `AUTOMATED_DECISION_FULLY` (2), by `automated` bits 1 and 2 | statement | closed | | `puid` | the decision's digest, 64 lowercase hexadecimal digits | the decision | `[a-zA-Z0-9-_]`, up to 500, unique | The policy is the one in effect at the decision's `t`. A notice of ground 2 is not a notice under Article 16, and the database's documentation names `SOURCE_TYPE_OTHER_NOTIFICATION` for it. A decision on a complaint keeps the source of the restriction it answers. The database's other members are not written: `decision_monetary`, `decision_provision` and `decision_account` (no WAI decision restricts payments, the service or an account), their `_other` texts, `decision_visibility_other`, `account_type`, `incompatible_content_illegal`, `category_addition`, `category_specification` and `category_specification_other`, `content_id` (a product number), `content_language`, every `end_date_*` (a restriction lasts until a decision supersedes it, which the database reads as indefinite when the date is left out), and `source_identity` (§9.7). ### 9.4 Article 17, element by element | Article 17 | what it asks for | WAI's field | the member | |---|---|---|---| | (1)(a) | a restriction of the visibility of specific items | `verdict` deny or redact | `decision_visibility` | | (1)(b)–(d) | payments, the service, the account | none | not written | | (3)(a) | removal, disabling, demotion or restriction; territorial scope; duration | `verdict`; policy `territory`; the line (a restriction lasts until superseded) | `decision_visibility`; `territorial_scope`; no end date | | (3)(b) | facts and circumstances; whether on a notice or on own initiative; the notifier's identity where strictly necessary | statement `facts`; `basis` and the notice; none | `decision_facts`; `source_type`; not written | | (3)(c) | use of automated means, in the decision and in detection | statement `automated` | `automated_decision`, `automated_detection` | | (3)(d) | for illegal content: the legal ground and why the item is illegal on it | statement `ground` 1, `ground_ref` or the code's `reference`, `explanation` | `decision_ground`, `illegal_content_legal_ground`, `illegal_content_explanation` | | (3)(e) | for incompatibility: the contractual ground and why the item is incompatible with it | statement `ground` 2, `ground_ref` or the code's `reference`, `explanation`; the code's `url` | `decision_ground`, `incompatible_content_ground`, `incompatible_content_explanation`, `decision_ground_reference_url` | | (3)(f) | the possibilities for redress | policy `redress`; a complaint (§5) and its decision (§4.5) | none: the database has no member for it; it is the statement to the affected participant's | | the category of the decision | (the database's, from Implementing Regulation (EU) 2024/2835 Annex II) | the reason code's `category` | `category` | | the item's kind and date | (the database's) | the target | `content_type`, `content_date` | | the decision's date and identifier | (the database's) | the decision's `t` and digest | `application_date`, `puid` | ### 9.5 Categories | number | `category` | |---|---| | 1 | `STATEMENT_CATEGORY_ANIMAL_WELFARE` | | 2 | `STATEMENT_CATEGORY_CONSUMER_INFORMATION` | | 3 | `STATEMENT_CATEGORY_CYBER_VIOLENCE` | | 4 | `STATEMENT_CATEGORY_CYBER_VIOLENCE_AGAINST_WOMEN` | | 5 | `STATEMENT_CATEGORY_DATA_PROTECTION_AND_PRIVACY_VIOLATIONS` | | 6 | `STATEMENT_CATEGORY_ILLEGAL_OR_HARMFUL_SPEECH` | | 7 | `STATEMENT_CATEGORY_INTELLECTUAL_PROPERTY_INFRINGEMENTS` | | 8 | `STATEMENT_CATEGORY_NEGATIVE_EFFECTS_ON_CIVIC_DISCOURSE_OR_ELECTIONS` | | 9 | `STATEMENT_CATEGORY_NOT_SPECIFIED_NOTICE` | | 10 | `STATEMENT_CATEGORY_OTHER_VIOLATION_TC` | | 11 | `STATEMENT_CATEGORY_PROTECTION_OF_MINORS` | | 12 | `STATEMENT_CATEGORY_RISK_FOR_PUBLIC_SECURITY` | | 13 | `STATEMENT_CATEGORY_SCAMS_AND_FRAUD` | | 14 | `STATEMENT_CATEGORY_SELF_HARM` | | 15 | `STATEMENT_CATEGORY_UNSAFE_AND_PROHIBITED_PRODUCTS` | | 16 | `STATEMENT_CATEGORY_VIOLENCE` | These are the sixteen values the database accepts in this version, in its order. Category 9 is for a notice that does not specify the kind of illegal content (Implementing Regulation (EU) 2024/2835, Annex II). ### 9.6 Content types | target | `content_type` | `content_type_other` | |---|---|---| | `chat`, `caption`, `poll_open`, `marker` | `CONTENT_TYPE_TEXT` | | | `reaction` | `CONTENT_TYPE_OTHER` | `reaction` | | `vote` | `CONTENT_TYPE_OTHER` | `poll vote` | | `poll_close` | `CONTENT_TYPE_OTHER` | `poll tally` | | `meta` | `CONTENT_TYPE_OTHER` | `timed metadata` | | `cue` | `CONTENT_TYPE_OTHER` | `cue` | | an object of media 1 (audio) | `CONTENT_TYPE_AUDIO` | | | an object of media 2 (video) or 3 (screen) | `CONTENT_TYPE_VIDEO` | | ### 9.7 Personal data Article 24(5) has the information submitted to the database contain no personal data. The export writes nothing from a principal's key, a notice's or a complaint's `contact`, `statement` or `submitter`, or the content of the item: its members come from the decision's verdict, `t` and digest, the target's kind and `t`, the policy's vocabulary and territory, and the statement's three texts. It never writes `source_identity`, which the database documentation reserves for the notifier's identity where strictly necessary. The statement's texts are copied as their author wrote them; whether they hold personal data is a question about their content that no verifier can answer, and they are written knowing that they are published. ### 9.8 When no record can be written The record is written from the members in the order of §9.3. The first member that cannot be written as §9.3 has it names the refusal, `sor_incomplete`, stated with that member's name: - the decision has no statement, or an unknown one: `decision_facts` (and every member drawn from the statement after it); - a text that is empty, or longer than the member allows: that member; - the target is a withheld target or an unresolved one, so its kind and `t` are not held: `content_type`; - a date outside the range the member allows: that member; - a decision of basis 3 whose superseded decision's source cannot be written: `source_type`. ## 10. Reasons The codes this extension adds, each with where it is raised: | reason | raised at | |---|---| | `policy_malformed` | §3.3 | | `policy_unlinked` | §3.4 | | `policy_conflicts_room` | §3.5 | | `notice_malformed` | §5.2 | | `statement_misplaced` | §7.2 step 4 | | `statement_malformed` | §4.3, §7.2 step 4 | | `reason_not_in_policy` | §7.4 step 1 | | `target_unknown` | §7.4 step 2 | | `target_not_decidable` | §4.1, §7.4 step 2 | | `target_prev_mismatch` | §7.4 step 2 | | `not_reviewer` | §4.4, §7.4 step 3 | | `supersede_gap` | §4.5, §7.4 step 4 | | `supersede_unknown` | §4.5, §7.4 step 4 | | `supersede_mismatch` | §4.5, §7.4 step 4 | | `notice_unknown` | §5.2, §7.4 step 5 | | `notice_mismatch` | §5.2, §7.4 step 5 | | `fallback_not_in_policy` | §4.6, §7.4 step 5 | | `fallback_early` | §4.6, §7.4 step 5 | | `sor_incomplete` | §9.8, with the member | | `redaction_not_applied` | §8.3 | A moderation verifier also raises every code a room verifier raises (live-rooms §11 and the live-events codes it lists), at the room: `kind_not_permitted` among them for a decision by a principal without the moderate power (§4.4); `truncated`, `malformed` and `unknown_version` for a policy or a notice that does not read; `wrong_room`, `bad_key` and `bad_signature` for a policy or a notice of another room or whose signature fails; and `delivery_gap` for a gap that remains, which is not a refusal. A clip under moderation raises every code a clip verifier raises (live-recording §9), and `redaction_not_applied`. ## 11. Conformance [`live-conformance/moderation/`](../live-conformance/moderation/) holds the corpus, written from fixed seeds by `wai_live_moderation_vectors gen`: `vectors.json`, and each export as a file of its own, `NAME.dK.sor.json`, *K* the decision's position among the vector's decisions. It has: - **Rooms**, each a room (descriptor, grants, key-log head or none, chains), its policies, notices and objects, and the outcome §7.5 gives them with everything it states: - a chat room under a pre-moderation policy, replaced partway by a second policy that also reviews reactions after delivery: one message allowed before its deadline, one denied, one past its deadline taking the fallback, which the clock signs; a reaction reviewed after delivery whose fallback holds it; - the same room as a subscriber that is not a reviewer receives it, the denied message and the fallback's withheld, the gap they leave explained by the decisions; - a notice and its decision, and a trusted flagger's notice and its redaction; - an appeal: a complaint against a decision taken with automated means, and the decision that reverses it; - a room whose recorded take has a chunk redacted, and the same room after an appeal restores it; - one refused room for each reason of §10 a room raises, each a control of one defect, and a decision by a principal without the moderate power, which the room refuses `kind_not_permitted`; - a room with a gap that no decision explains. - **Exports**: every decision's record, byte for byte, none for an allow, and one refused `sor_incomplete`. - **Clips** under moderation over the redacted take: one that withholds the redacted chunk, one cut by bytes whose first chunk is the redacted one, one that presents the redacted chunk's bytes (`redaction_not_applied`), and, after the appeal, one that leaves the restored chunk out (`clip_chunk_missing`) and one that presents it. A conforming verifier gives every room, export and clip the outcome, the reason, the place and everything else the corpus states. `wai_live_moderation_vectors verify` and the test `tests/live_moderation.rs` check that every file regenerates byte for byte, that `wai-rs` gives every case its outcome, that every reason of §10 appears, and that §10 lists exactly the reference's codes. `live-conformance/live_moderation_verify.py` is the second implementation, written from this text: it shares no code with `wai-rs`; it verifies each room with the live-rooms corpus's second implementation (`live_rooms_verify.py`), each clip with the live-recording corpus's (`live_record_verify.py`), and every signature with WAI's strict rule (`claim-conformance/_ed25519.py`); it writes every export from §9 and requires its bytes; and it mutates copies of the corpus and requires every copy to fail. ## 12. Security and privacy considerations - Every decision is signed by its moderator, every statement rides in the moderator's chain, every policy is signed by the room authority, and every notice by its submitter. A relay can withhold any of them but alter none, and a withheld decision is a gap in its moderator's chain. - A relay holds the power to withhold. A subscriber checks it: an event withheld with no restricting decision behind it is a delivery gap that §7.3 does not explain, which the subscriber sees and can report. A relay that delivers a restricted item to a session is caught by the decision the session also holds. - Explaining a gap needs each restricted event's `prev`, which its moderator states. A moderator that states a false one makes the gap unexplained, never a forged event accepted: the chain still has to reach the event before the gap. - Withholding is not erasure. A denied item stays in the recording, and a redacted one stays wherever a copy was delivered before the decision; a redaction stops what WAI's relays, recordings and clips deliver from then on. - A decision names its target by digest only, and its statement holds no content. A notice holds its submitter's contact, which is personal data: a verifier is given notices to check decisions against them, and no export or derived record carries a notice's texts or its key. - The statement's texts are published with the decision and with its export. Whoever writes them writes them for a public record. - Fallback decisions are signed by the room's clock key, which is the relay's; a policy limits them to the fallback it names, at or after the deadline it names, so a relay cannot restrict an item for a reason the policy does not hold. - The `automated` field is its moderator's statement. Nothing in a record shows how a verdict was reached; the field states it, and an export carries it.