Skip to main content

WAI Extension: Live Ads

Mirrored from the canonical text at commit b1681667 ().

Status: Draft. Registers the record wai.live.ads: server-guided ad insertion for rooms. The room authority signs an ad policy, WAP1, naming the grant its ad decider runs under and how the room’s breaks are presented. The breaks themselves are the room’s signed cue events (live-events §4.3), an out and its in, or a break of known duration, with a post-roll marked by a break annotation, WAB1, in the cue author’s chain. Each break maps, deterministically, onto one interstitial date range of HLS and one alternative-presentation event of DASH, so every edge writes the same playlist and MPD bytes and the manifests stay shared and cacheable. Per-viewer decisioning happens behind the one URI each break names: the asset list a player fetches when it reaches the break, filled by wai.video.ssai’s deterministic decision. Each viewer’s impressions are sealed in a wai.video.ssai receipt of its break form, which names the break, the decision’s inputs and the asset list served, and a verifier recomputes the decision, the asset list and the spend against the decider grant’s funds ceiling. A policy of version 2 can require that each impression count only when the player’s signed presentation claim verifies (ad-presentation), and the spend is then held to the ceiling counting only those impressions. No new transport. The reference implementation is the live_ads module of wai-rs (feature live_ads) with the break form in wai_video_ssai; the corpus is live-conformance/ads/, and its second implementation is live-conformance/live_ads_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: draft-pantos-hls-rfc8216bis-22 (1 May 2026), its §4.4.5.1 (EXT-X-DATERANGE), §6.2.1, §6.2.4, Appendix D (Interstitials) and Appendix F (Preloading HLS Date Range Resources). Its Appendix H is a glossary and defines no date-range schedule, so nothing here maps onto it. ISO/IEC 23009-1:2026, the sixth edition, through the XML schema that accompanies it (its types EventType, AlternativeMPDEventType, AlternativeMPDReplaceEventType, ImportedMpdType and PresentationType) and the schema’s example MPDs G23, G29-1 and G29-2, at the schema’s revision of 2026-06-13; the clause text of its §5.16 was not available to this revision (§6.1). A section number of one of these texts is that revision’s.

1. Scope

In scope: the ad policy, its signature and rules (§3); the room’s breaks, derived from its cue events and break annotations, with their identity and refusals (§4); their HLS interstitial date ranges (§5) and their DASH alternative-presentation events (§6); the asset list, the decision that fills it, and a viewer joining a break already running (§7); the break form of the wai.video.ssai receipt (§8); and verification end to end, from the room’s events to the spend (§9).

Out of scope: a transport; how a decider assembles each viewer’s candidate pod (targeting); how beacons reach it; the SCTE-35 messages a cue may carry, which WAI carries verbatim and never reads (live-events §4.3); the stitched form of wai.video.ssai, which is unchanged (§8.1); and a pre-roll, which a room does not have: a viewer who joins a running break is given what remains of it (§7.4), and one who joins between breaks starts with the program.

Relationship to the core spec: wai.live.ads is a record (SPEC §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable.

Relationship to live-events: this extension changes nothing in it. A break is opened and closed by cue events as they are; the break annotation rides in the meta kind (§4.2 there), bound to its cue by the chain, as timed-text’s annotation is bound to its caption. Live-events’ own date range of each cue (§8.3 there) is written beside the interstitial date range of this extension, under another ID and another CLASS.

Relationship to live-rooms: a cue’s author is held to its grant: a principal whose grant in effect does not permit cue is refused by the room verifier, kind_not_permitted, before this extension reads its cue (live-rooms §9.2 step 4). The decider runs under a room grant, whose funds ceiling bounds the room’s ad spend (§3.4), and names it in its receipts as live-rooms §4.5 has every receipt name a grant.

Why a record of its own, beside wai.video.ssai: the policy, the breaks and their renderings are verified against a room and its events track, which the stitched form never reads; the impressions are wai.video.ssai’s, in a second form of its receipt (§8), so that one decision rule and one spend rule serve stitched and server-guided insertion alike, and every receipt of the stitched form keeps its bytes.

2. Terms

Integers in the records of §3 and §4 are little-endian, and a digest there is SHA-256 (FIPS 180-4), 32 bytes. Integers of the receipt’s signed bytes (§8) are big-endian and its hashes BLAKE3, as the stitched form’s are. 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 long field is a u16 length and that many bytes.

3. The ad policy

3.1 Layout

offsetsizefieldmeaning
04magicWAP1 (ASCII)
41version1, or 2 for a policy with presentation fields
532roomthe room id (live-rooms §3.2)
3732decider_grantthe digest of the room grant the decider runs under (§3.4)
691slotsthe most ads one break holds, 1 to 16
704open_avail_msu32: the avail of a break opened with no planned duration (§4.3); 0: such a break is refused
744preload_msu32: how long before a break a player may fetch its asset list (§5.3, §6.2); 0: no preload
781restrictbit 0 skip: no seeking forward in a break; bit 1 jump: no seeking past a break unplayed
792skip_after_su16: seconds of a break played before a viewer may skip the rest; 65535: no skip control
811snapbit 0 out: leave the program at the segment boundary nearest the break’s start; bit 1 in: return at the one nearest its end
82longasset_basethe base of every asset-list URI (§7)
…longcreative_basethe base of every creative’s URI (§7)
…4 + longpresentation fieldsversion 2 only: presentation, viewability, token_type and issuer, as ad-presentation §8 defines them
…64sig§3.2

A base is https:// followed by 1 to 492 bytes, each of ! to ~ (0x21–0x7E) other than ", #, &, ', <, >, ? and \, the last of them /. A base so holds no character that HLS’s quoted strings, JSON strings or XML attribute values would have to escape, and a URI built on it (§7) is the base followed by a name of the break or of the creative.

3.2 Digest and signature

The digest of a policy is SHA-256 over its record from magic through creative_base, or through issuer for a policy of version 2. The signature is Ed25519 by the room descriptor’s authority over the 20 bytes wai:live-ads-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 WAP1: malformed. No byte after the magic: truncated. A version other than 1 or 2: unknown_version.
  2. The bytes end before a field the layout requires: truncated.
  3. Bytes after sig: malformed.

It then refuses as ad_policy_malformed a policy whose slots is not 1 to 16, whose restrict or snap sets a bit above bit 1, whose asset_base or creative_base is not a base (§3.1), or, of version 2, whose presentation fields break a rule of ad-presentation §8.

3.4 The decider

The decider is the principal of the grant whose digest is decider_grant. That grant is one of the room’s grants, which the room verifier checks (live-rooms §4.3); it need not be used in a join, since a decider sends no event. Its funds_ceiling and currency are the room’s ad budget: every price and every spend of §7 and §8 is in millionths of a minor unit of currency, as live-metering’s tariff prices are, so a spend of s is within a ceiling of c minor units when s ≤ c × 10^6. A funds_ceiling of 2^64 − 1 states no ceiling. The decider’s receipts name the grant as live-rooms §4.5 has a receipt name one (§9.3).

A grant is good for a break when it is good at the break’s t (live-rooms §4.3), and it is not revoked through the authority’s key log (live-rooms §5).

4. Breaks

4.1 Cues

A cue (live-events §4.3) carries a type, a cue_id, a duration_ms and, optionally, an SCTE-35 message this extension never reads. Its t is the instant of the splice on the room clock: a publisher MAY send a cue before that instant, and a cue anchored to a media object (live-events §3.1) splices at that object. Every cue the room accepted, of any principal permitted cue, the room keys among them, is read here, in the merged order (live-rooms §9.2), each once.

4.2 The break annotation

A break is mid-roll unless its author annotates it as a post-roll. The annotation is the payload of a meta event whose first four bytes are WAB1, by the same principal, at the cue’s seq + 1 and at the cue’s t:

offsetsizefieldmeaning
04magicWAB1 (ASCII)
41version1 in this revision
532cuethe digest of the cue it annotates
371placement1 mid-roll, 2 post-roll

An annotation is refused annotation_malformed when it is not 38 bytes, its version is not 1, or its placement is not 1 or 2. It is refused annotation_misplaced unless the event before it in its author’s chain, at seq − 1, is a cue of type break, at the same t, whose digest is cue.

A post-roll is the break of an annotation of placement 2. It ends the program: after it, the room has no break and no cue (§4.3, cue_after_close), and a room that closes ends with it. A player plays it after the program has ended (§5.1, §6.2).

4.3 The schedule

The schedule is the room’s breaks and chapters, in the order of the cues that open them. Over the cues and annotations in the merged order, holding the open break (at most one) and the end of the last closed break, a verifier refuses, at the record:

A break open at the end is stated open: the room may still be live. A cue of the room that does not read is the room’s refusal, and an event with a gap before it is a delivery gap the room states (§9.2 step 1).

4.4 A break’s identity

A break is named by the digest of the cue that opens it. From that digest:

Every edge holding the room’s events derives the same identity, and no other event of the room has it: live-events’ own date range of the cue has the digest alone as its ID.

5. HLS: interstitial date ranges

5.1 The interstitial tag

Each break is one Interstitial EXT-X-DATERANGE tag (draft-pantos-hls-rfc8216bis-22 §4.4.5.1, Appendix D.2), written into every Media Playlist of the room when its cue is known. Its attributes are written in this order, separated by commas with no spaces, each only where its row says:

attributevalue
IDquoted: the break’s ID (§4.4)
CLASSquoted: com.apple.hls.interstitial, the class Appendix D.2 defines
START-DATEquoted: t0 + t of the opening cue, as YYYY-MM-DDTHH:MM:SS.mmmZ in UTC, the milliseconds floored (live-events §8.3)
CUEquoted: ONCE for a mid-roll, POST,ONCE for a post-roll (§4.4.5.1)
DURATIONa break of type 4: its duration as decimal seconds with three digits after the point (30.000)
PLANNED-DURATIONan out with a planned duration: that duration, as DURATION
X-ASSET-LISTquoted: the break’s HLS asset-list URI (§7.2)
X-RESUME-OFFSETa mid-roll of type 4: its duration, as DURATION
X-PLAYOUT-LIMITa break of type 4: its duration, as DURATION
X-SNAPwhen snap is not 0: quoted, OUT, IN or OUT,IN by its bits
X-RESTRICTwhen restrict is not 0: quoted, SKIP, JUMP or SKIP,JUMP by its bits
X-SKIP-CONTROL-OFFSETwhen skip_after_s is not 65535: skip_after_s in decimal (Appendix D.3)

5.2 The closing tag

When an out’s in closes it, the break gains a second tag with the same ID, as §6.2.1 has a server signal the actual end of a date range with a planned duration, and as §4.4.5.1 lets a later tag add attributes to a date range:

attributevalue
IDquoted: the break’s ID
CLASSquoted: as §5.1
DURATIONthe break’s duration (§4.3), as §5.1 writes durations
X-RESUME-OFFSETthe same value
X-PLAYOUT-LIMITthe same value

No attribute of the first tag changes: the closing tag adds three it did not have, and repeats CLASS with its value. A player inside the break when the closing tag arrives ends the break at the playout limit, and returns to the program where the in placed its end.

5.3 The preload tag

When the policy’s preload_ms is not 0, each break also has a Preload EXT-X-DATERANGE tag (Appendix F, Appendix D.2), written just before its interstitial tag:

attributevalue
IDquoted: the break’s ID followed by -preload
CLASSquoted: com.apple.hls.preload, the class Appendix F defines
START-DATEquoted: the interstitial’s START-DATE less preload_ms, in its form
DURATIONpreload_ms, as §5.1 writes durations
X-URIquoted: the break’s HLS asset-list URI (§7.2)
X-TARGET-IDquoted: the break’s ID
X-TARGET-CLASSquoted: the interstitial class

A player picks a random instant in the preload range and fetches the asset list there (Appendix D.2), so the requests of a room’s audience for one break are spread over preload_ms rather than arriving together at its start.

5.4 What stays shared

Every attribute of §5.1 to §5.3 is a function of the signed cue, its annotation, the signed policy and the room’s t0, and of nothing a viewer supplies. Every Media Playlist of the room therefore carries the same date ranges with the same attributes, as §6.2.4 requires, and every edge that holds the room’s events writes the same bytes, so playlists stay cacheable. The one URI a break names is where each viewer’s decision is made (§7), and the request for it carries what makes it a viewer’s: the _HLS_primary_id and _HLS_start_offset query parameters Appendix D.4 has a player add.

A playlist that carries these tags carries EXT-X-PROGRAM-DATE-TIME on the same UTC timeline as t0 (§4.4.5.1). The corpus states the tags of each room in the order a packager writes them: a break’s preload and interstitial tags at its cue, its closing tag at its in.

6. DASH: alternative presentation events

6.1 What this revision read

ISO/IEC 23009-1:2026 defines alternative presentation events in its §5.16, whose text this revision could not read. It read the sixth edition’s MPD schema and its example MPDs. From them it takes: the event schemes urn:mpeg:dash:event:alternativeMPD:replace:2025 and urn:mpeg:dash:event:alternativeMPD:insert:2025 (example G23 and G29-1 use the first); an Event’s presentationTime, duration and id; its child ReplacePresentation or InsertPresentation, with the attributes uri (required), earliestResolutionTimeOffset, maxDuration, executeOnce, skipAfter and, for a replacement, returnOffset, with their types; and the list MPD, an MPD of type list whose Periods each import one MPD (ImportedMPD). The values below are written from those types and examples. Where the schema has an attribute whose meaning this revision could not state, it does not write it: noJump, serviceDescriptionId, startWithOffset and clip, whose defaults stand. A later revision checks every value here against §5.16.

6.2 The event

A mid-roll is an Event of the replace scheme; a post-roll is one of the insert scheme, since after the program there is nothing to replace. With timescale 1000 on its EventStream:

attributevalue
presentationTimept0 + ⌊t / 1000⌋, t the opening cue’s, where pt0 is the presentation time of t0 in milliseconds on the Period’s timeline (live-events §8.4)
durationwhen the break’s duration is known (a break of type 4, or an out its in has closed): that duration in milliseconds
idthe break’s event id (§4.4)

and its one child, ReplacePresentation for a mid-roll and InsertPresentation for a post-roll:

attributevalue
urithe break’s DASH asset-list URI (§7.3)
earliestResolutionTimeOffsetwhen preload_ms is not 0: preload_ms
maxDurationwhen the duration is known: the duration in milliseconds
executeOncetrue, as HLS’s ONCE
skipAfterPT, then skip_after_s in decimal, then S; with no skip control, the avail in whole seconds rounded up (PT30S for 30 000 ms), so that no viewer skips before the avail has played: the schema’s examples read a missing skipAfter as a break a viewer may skip at once
returnOffseta ReplacePresentation whose duration is known: the duration in milliseconds, as HLS’s X-RESUME-OFFSET

Attributes are written in the order of the two tables, each as name="value", the element as <Event …><ReplacePresentation …/></Event>, with no whitespace other than one space before each attribute. An out’s event is written first without duration, maxDuration and returnOffset, and the MPD that follows its in writes the same event with them: an MPD update gives the event its end as HLS’s closing tag does (§5.2). A planned duration has no DASH attribute.

6.3 The event streams

The Period that holds the room’s program carries, after every break of the schedule is written:

  1. when the schedule has a mid-roll, <EventStream schemeIdUri="urn:mpeg:dash:event:alternativeMPD:replace:2025" timescale="1000">, the mid-rolls’ events in schedule order, and </EventStream>;
  2. when it has a post-roll, the same with the insert scheme and the post-roll’s event.

Every value is a function of the signed records, the policy and pt0, so the MPD stays one for every viewer, as the playlists do (§5.4).

7. The asset list

7.1 The decision

A player that reaches a break fetches its asset list. The decider fills it for that viewer with the decision of wai.video.ssai (SPEC §5), unchanged:

The fill is a pure function of those inputs, so anyone holding them recomputes it, and the receipt binds each of them (§8).

7.2 HLS asset list

The break’s HLS asset-list URI is asset_base, the break’s ID, .json?_HLS_interstitial_id= and the ID again: the query parameter Appendix D.4 has a packager add. The response is the JSON object Appendix D.2 defines, written exactly:

{"ASSETS":[{"URI":"<creative_base><creative_hash>.m3u8","DURATION":<s>},…]}

one Asset-Description for each ad of the fill in slot order, <creative_hash> its 64 lowercase hexadecimal digits and <s> its duration_ms as decimal seconds with three digits after the point, with no whitespace and no byte after the closing brace. An empty fill is {"ASSETS":[]}, which has a player resume the program without playing an ad (Appendix D.5). The asset list carries no SKIP-CONTROL: the tag’s attributes stand.

A creative’s URI names it by its hash, so the asset list a viewer is served binds the creative bytes the receipt names.

7.3 DASH list MPD

The break’s DASH asset-list URI is asset_base, the break’s ID and .mpd. The response is a list MPD, written exactly as the XML declaration <?xml version="1.0" encoding="UTF-8"?>, a line feed, and:

<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:list:2024" type="list" minBufferTime="PT1S"><Period id="0" duration="PT<s>S"><ImportedMPD><creative_base><creative_hash>.mpd</ImportedMPD></Period>…</MPD>

one Period for each ad of the fill, its id the slot in decimal and <s> as §7.2, with no whitespace other than that shown and no byte after </MPD>. The profile and minBufferTime are those of the schema’s list-MPD example (G29-2). An MPD has at least one Period, so an empty fill has no list MPD: the decider answers the request with no body, and the receipt binds the empty asset list, zero bytes (§8.2).

7.4 Joining a break

A viewer who reaches a break after its start, by joining the room inside it, has its player add the offset into the break at which it starts (_HLS_start_offset, Appendix D.4). The decider fills the remaining avail only (§7.1), and the receipt states the offset (start_offset_ms). A viewer who joins between breaks is given no break: a room has no pre-roll. The asset list a joining viewer is served is a fill like any other, and every check of §9.3 applies to it.

8. The break form of the wai.video.ssai receipt

8.1 Forms

A wai.video.ssai receipt has two forms:

The two forms sign under different domains (§8.4), so no signature of one is a signature of the other, and they share the impression, the pod hash, the Merkle root, the billable spend and the identity domain, so a figure profile of energy-measurement §6.1, wai:video-ssai-receipt-id, names either.

8.2 Fields and their JSON carriage

A form-2 receipt is carried as one JSON object (RFC 8259) with exactly these members; a byte field is lowercase hexadecimal of its exact length, and an integer a JSON integer from 0 to 2^64 − 1:

membervalue
form2
room32 bytes: the room id
cue32 bytes: the break’s digest (§4.4)
session16 bytes: the session (§2), one value for every break of one playback session, and no other’s
start_offset_msthe viewer’s offset into the break (§7.4); 0 for a viewer present at its start
formatfrom 0 to 255: 1, the HLS asset list (§7.2); 2, the DASH list MPD (§7.3)
asset_list_sha25632 bytes: SHA-256 of the asset list served, its exact bytes
avail_msthe break’s avail (§4.3)
slotsthe policy’s slots
funds_remaining_microsthe funds the decision ran with (§7.1)
pod_hash32 bytes: §8.3
impressionsan array: each filled slot, {"slot", "ad_id", "creative_hash", "duration_ms", "price_micros", "quartiles_reached"}, in slot order; ad_id a string, creative_hash 32 bytes, quartiles_reached 0 to 255
merkle_root32 bytes: §8.3
budget_spent_microsthe billable spend (§8.3)
work_decide_opsthe candidates the decision considered
joules_microthe decision’s energy, or 0 unmetered
energy_provenanceits class’s label (energy-measurement §2.7: ModelBased, Estimator, OnChipCounter, CalibratedInstrument), or null unmetered
energy_uncertainty, energy_counter_filtering, energy_calibration_refonly for a class that carries them, as energy-measurement §2.7 names and writes them: {"relative_ppm", "window_us"}, relative_ppm from 0 to 2^32 − 1; Unknown, Off or On; a string
grant{"grant_hash", "capability", "joule_ceiling_micro", "funds_ceiling"}: the grant reference (live-rooms §4.5), funds_ceiling null for none
signer_pubkey32 bytes: the decider’s Ed25519 key
signer_ida string: the decider’s did:key or origin-bound id
sig64 bytes: §8.4

quartiles_reached is the stitched form’s: 0 never rendered, 1 started (the billable impression), 2 to 5 a quarter, a half, three quarters and all of it. A receipt that does not read so, or that carries the legacy class label HwShunt (energy-measurement §2.6), is refused receipt_malformed.

8.3 Hashes

BLAKE3, with integers big-endian, as the stitched form computes them:

8.4 Signing payload and identity

The signing payload is the concatenation of:

  1. wai:video-ssai-break-receipt (ASCII) and 0x01;
  2. room, cue, session, start_offset_ms (u64), format (u8), asset_list_sha256, avail_ms (u64), slots (u64), funds_remaining_micros (u64);
  3. pod_hash, merkle_root, budget_spent_micros (u64), work_decide_ops (u64), joules_micro (u64), then the class in its signed encoding (energy-measurement §2.7), the tag byte 0 for none;
  4. the grant reference: grant_hash, the length (u32) and bytes of capability, joule_ceiling_micro (u64), then 1 and funds_ceiling (u64), or 0 for none;
  5. the impressions’ count (u64), then each one’s leaf;
  6. signer_pubkey, then the length (u32) and bytes of signer_id.

sig is Ed25519 by signer_pubkey over the signing payload. The receipt’s identity, its receipt hash, is BLAKE3 over wai:video-ssai-receipt-id 0x01, the signing payload and sig: the stitched form’s identity domain, over a payload whose first bytes no stitched payload has.

8.5 Its own rules

A form-2 receipt holds, on its own:

  1. format is 1 or 2;
  2. its signature verifies (bad_key, bad_signature);
  3. merkle_root is the root of its impressions, budget_spent_micros their billable spend, and when grant’s funds_ceiling is not null, the spend is within it (§3.4);
  4. the impressions’ slots are 0, 1, 2, … in order, each quartiles_reached is at most 5, and work_decide_ops is at least their count;
  5. the figure and its class cohere as energy-measurement §2 has them: joules_micro 0 with no class, or above 0 with a class, a hardware class’s declared uncertainty with relative_ppm and window_us above 0 and at the resolution floor (joules_micro × relative_ppm at least 288 676), and a calibrated instrument’s reference holding a character that is not Unicode White_Space.

A rule of 1, 3, 4 or 5 that fails is receipt_invalid. These rules are what a buyer holding one receipt checks alone; §9.3 adds what the room, the policy and the pod let a verifier check.

9. Verification

9.1 Inputs

An ads 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:

9.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 ads verifier’s, at room with the room verifier’s own place. A gap is the outcome gap: a schedule read from part of the cues is not the room’s, so the verifier states the gaps and nothing more.
  2. The policy, at policy: it reads (§3.3: truncated, malformed, unknown_version, ad_policy_malformed); its room is the room id (wrong_room); its signature verifies under the descriptor’s authority (bad_key, bad_signature); decider_grant is the digest of a grant the room verifier holds (decider_grant_unknown); and that grant is not revoked through the key log the room verifier read (live-rooms §5, grant_revoked).
  3. The schedule (§4.2, §4.3), over the events the room accepted in the merged order, each once: the refusal, at the record’s chain and its index in that chain.
  4. The renderings (§5, §6): the tags, the events and the asset-list URIs of every break. No refusal.
  5. The receipts, in the list’s order, each by §9.3: the refusal, at receipt and its index. Under a policy that requires presentation claims, the claims are read before the first receipt, and each receipt’s claims are judged after its step 11 (ad-presentation §9.2); a claim’s verdict never refuses the room.
  6. The outcome (§9.4).

9.3 Each receipt

For receipt k, with its pod, in this order:

  1. It reads as §8.2 has it: receipt_malformed.
  2. Its room is the room id: wrong_room.
  3. Its own rules (§8.5): bad_key, bad_signature or receipt_invalid.
  4. signer_pubkey is the decider (§3.4): receipt_not_decider.
  5. grant is the decider grant’s reference: grant_hash its digest, capability wai.live.room, joule_ceiling_micro its joules_ceiling, and funds_ceiling its funds_ceiling, or null when that is 2^64 − 1 (live-rooms §4.5): receipt_grant_mismatch.
  6. cue names a break of the schedule: receipt_break_unknown.
  7. The decider grant is good at the break’s t (§3.4): outside_grant_window.
  8. pod_hash is the pod hash (§8.3) of the pod given: pod_mismatch.
  9. avail_ms is the break’s avail, slots the policy’s, and the impressions, less quartiles_reached, are the fill §7.1 gives for the pod, the slots, the avail less start_offset_ms and funds_remaining_micros: decision_mismatch.
  10. asset_list_sha256 is SHA-256 of the asset list of the fill in the receipt’s format (§7.2, §7.3): asset_list_mismatch.
  11. No receipt before it in the list names this cue and this session: receipt_duplicate. A viewer is filled once for a break.
  12. Spend-equivalence. funds_remaining_micros is within the decider grant’s ceiling, and so is the spend that counts of this receipt and every receipt before it, together (§3.4): otherwise funds_exceeded. The spend that counts is a receipt’s billable spend, or, under a policy that requires presentation claims, its presented spend: the prices of the impressions whose claim verifies (ad-presentation §9.3).

Step 12 holds the room’s ad spend to one ceiling across every viewer and break, in the order the receipts are given. A decision’s stated funds are the decider’s statement; that the spend of every receipt together stays within the ceiling is checked, whatever was stated.

9.4 Outcomes and what a verification states

With accept, the verifier states:

10. Reasons

The codes this extension adds, each with where it is raised:

reasonraised at
ad_policy_malformed§3.3, §9.2 step 2
decider_grant_unknown§9.2 step 2
annotation_malformed§4.2
annotation_misplaced§4.2
cue_after_close§4.2, §4.3
break_overlap§4.3
break_duration_unknown§4.3
break_zero_duration§4.3
cue_in_unmatched§4.3
cue_duration_mismatch§4.3
receipt_malformed§8.2, §9.3 step 1
receipt_invalid§8.5, §9.3 step 3
receipt_not_decider§9.3 step 4
receipt_grant_mismatch§9.3 step 5
receipt_break_unknown§9.3 step 6
pod_mismatch§9.3 step 8
decision_mismatch§9.3 step 9
asset_list_mismatch§9.3 step 10
receipt_duplicate§9.3 step 11
funds_exceeded§9.3 step 12

An ads 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 cue or an annotation by a principal whose grant does not permit its kind; truncated, malformed and unknown_version for a policy that does not read; wrong_room, bad_key and bad_signature for a policy or a receipt of another room or whose signature fails; grant_revoked and outside_grant_window for the decider grant (§9.2 step 2, §9.3 step 7); and delivery_gap for a gap, which is not a refusal.

11. Conformance

live-conformance/ads/ holds the corpus, written from fixed seeds by wai_live_ads_vectors gen: vectors.json, and each accepted room’s HLS tags as a file of its own, NAME.m3u8, one tag a line in the order of §5.4. It has:

A conforming verifier gives every room the outcome, the reason, the place and everything else the corpus states, and every tag file byte for byte. wai_live_ads_vectors verify and the test tests/live_ads.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_ads_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), every signature with WAI’s strict rule (claim-conformance/_ed25519.py) and every BLAKE3 hash with claim-conformance/_blake3.py; it writes every tag, event and asset list from §5 to §7 and requires their bytes; and it mutates copies of the corpus and requires every copy to fail.

12. Security and privacy considerations