The change of wai/ad-consent-supply-chain (44d5f2264..110c53553, 2 commits), on the fix branch: its changelog entries as files of wai/changes/, its wai-conformance.yml edits as step files, corpus files and test tags, and every other file as the branch has it. - f8fe39cba WAI: an ad decision's consent signal and supply chain, bound into what the decider signs - 110c53553 WAI: a GPP string links what carries it; a chain per impression; sellers files read exactly Co-Authored-By: Claude Opus 5.5 <[email protected]>
65 KiB
WAI Extension: Live Ads
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 signedcueevents (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 bywai.video.ssai's deterministic decision. Each viewer's impressions are sealed in awai.video.ssaireceipt 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 thelive_adsmodule ofwai-rs(featurelive_ads) with the break form inwai_video_ssai; the corpus islive-conformance/ads/, and its second implementation islive-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 typesEventType,AlternativeMPDEventType,AlternativeMPDReplaceEventType,ImportedMpdTypeandPresentationType) 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.A receipt of version 2 also binds the request terms its decision was made under: the consent signal the player gave, a Global Privacy Platform string with the sections that apply and a child-directed flag (§8.6), and the supply chain each of its impressions was sold through, each seller named by its advertising system's domain and its seller id, with references to each system's sellers file and to the inventory owner's authorised-sellers file (§8.7). WAI carries the signal and the chain as the texts below define them, binds them into what the decider signs, and defines no scheme of its own beside them. A policy of version 3 requires every receipt to state its consent signal, child-directed in a room the policy marks as directed to children (§3.1, §9.3 step 13).
Texts read for §8.6 and §8.7, each at its source: Global Privacy Protocol String, version 1.0, its revision of 3 November 2023: its sections on what the string serves and holds, who makes it, its format, header, delimiters and section encoding, and how a service reached by a URL is given it. OpenRTB API Specification Version 2.6: §3.2.2 (
Source), §3.2.3 (Regs), §3.2.25 (SupplyChain) and §3.2.26 (SupplyChainNode). The OpenRTB SupplyChain object, in full. Sellers.json, version 1.0, July 2019, in full. ads.txt Specification Version 1.1, August 2022: §3 to §5. Authorized Sellers for Apps (app-ads.txt), version 1.0, March 2019: its abstract, its introduction, and its sections on publishing the file and on translating an app's developer URL into the file's path.
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).
The request terms a receipt of version 2 binds, and what a player, a decider and a verifier do
with them, are in scope too (§8.6, §8.7).
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.
Out of scope, too: what a GPP string's sections state, which the GPP string's texts define and
WAI does not decode; whether a viewer gave a consent signal, or any party honoured one; and how
sellers files and authorised-sellers files are fetched and kept fresh, which their own texts
say.
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
- Room, room clock, principal, event, chain, grant, room key, merged order: as in live-events §2 and live-rooms §2 and §9.2.
- Cue: a
cueevent (live-events §4.3). Its type is out (1), in (2), chapter (3) or break (4). - Break: a span of the room's program that ads replace, opened by a cue of type out or break (§4.3). A break is open from an out until its in, and closed otherwise.
- Post-roll: a break played after the program ends (§4.2).
- Avail: the duration, in milliseconds, a break's decision fills (§4.3).
- Decider: the principal that decides each viewer's fill and signs the receipts, under the grant the policy names (§3.4).
- Pod: the candidate ads a decision runs over for one viewer and one break, each an
ad_id, acreative_hash, aduration_msand aprice_micros(§7.1). - Fill: the ads a decision selects, in slot order.
- Asset list: the response to the request a player makes for a break: an HLS asset list (§7.2) or a DASH list MPD (§7.3).
- Session: one viewer's playback session, named in a receipt by 16 bytes the decider assigns (§8.2); under a policy that requires presentation claims, the 16 bytes the player's session key names (ad-presentation §3), which the decider does not choose.
- Consent signal, GPP string: §8.6.
- Supply chain, node, sellers file, authorised-sellers file, domain name: §8.7.
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
| offset | size | field | meaning |
|---|---|---|---|
| 0 | 4 | magic |
WAP1 (ASCII) |
| 4 | 1 | version |
1; 2 for a policy with presentation fields; 3 for one with presentation fields and an audience |
| 5 | 32 | room |
the room id (live-rooms §3.2) |
| 37 | 32 | decider_grant |
the digest of the room grant the decider runs under (§3.4) |
| 69 | 1 | slots |
the most ads one break holds, 1 to 16 |
| 70 | 4 | open_avail_ms |
u32: the avail of a break opened with no planned duration (§4.3); 0: such a break is refused |
| 74 | 4 | preload_ms |
u32: how long before a break a player may fetch its asset list (§5.3, §6.2); 0: no preload |
| 78 | 1 | restrict |
bit 0 skip: no seeking forward in a break; bit 1 jump: no seeking past a break unplayed |
| 79 | 2 | skip_after_s |
u16: seconds of a break played before a viewer may skip the rest; 65535: no skip control |
| 81 | 1 | snap |
bit 0 out: leave the program at the segment boundary nearest the break's start; bit 1 in: return at the one nearest its end |
| 82 | long | asset_base |
the base of every asset-list URI (§7) |
| … | long | creative_base |
the base of every creative's URI (§7) |
| … | 4 + long | presentation fields | version 2 and 3: presentation, viewability, token_type and issuer, as ad-presentation §8 defines them |
| … | 1 | audience |
version 3 only: bit 0 child: the room is directed to children (§8.6) |
| … | 64 | sig |
§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.
A policy of version 3 carries the presentation fields as a policy of version 2 does, all four
zero when it requires no claim, and then its audience. It requires every receipt to be of
version 2, stating the consent signal its decision was made under, and with child set a
child-directed one (§9.3 step 13).
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, or through audience for one of version 3.
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:
- Fewer than 4 bytes:
truncated. A magic other thanWAP1:malformed. No byte after the magic:truncated. A version other than 1, 2 or 3:unknown_version. - The bytes end before a field the layout requires:
truncated. - 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), of version 2 or 3, whose presentation fields break a rule of ad-presentation §8,
or, of version 3, whose audience sets a bit above bit 0.
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:
| offset | size | field | meaning |
|---|---|---|---|
| 0 | 4 | magic |
WAB1 (ASCII) |
| 4 | 1 | version |
1 in this revision |
| 5 | 32 | cue |
the digest of the cue it annotates |
| 37 | 1 | placement |
1 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:
- any cue after a post-roll's annotation:
cue_after_close; - out (1): when a break is open, or
tis before the end of the last closed break,break_overlap. Otherwise it opens a break: its planned duration isduration_ms, and its avail isduration_ms, or the policy'sopen_avail_mswhenduration_msis 0; with both 0,break_duration_unknown; - in (2): unless the open break was opened by an out of this
cue_idand of the in's author,cue_in_unmatched. The break's duration is ⌊(t− the out'st) / 1000⌋ milliseconds: when it is 0,break_zero_duration; when the in'sduration_msis not 0 and is not the duration,cue_duration_mismatch. The break is then closed, and its end is the in'st; - chapter (3): a chapter of the schedule, its
tand itsduration_ms(0 when not known). A chapter opens and closes nothing, and may fall inside a break; - break (4): when
duration_msis 0,break_zero_duration; when a break is open, ortis before the end of the last closed break,break_overlap. Otherwise it is a closed break whose duration and avail areduration_ms, ending att+duration_ms× 1000; - an annotation (§4.2): read and placed, or refused; of placement 2, its break is the post-roll.
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:
- its HLS ID is
wai-ad-followed by the digest's 64 lowercase hexadecimal digits; - its DASH event id is the digest's first eight bytes read as a big-endian u64, of which the low 53 bits are kept, so that a player that holds an id as a double holds it exactly. A receiver identifies a break by its digest; the id lets a player tell one event from another in an EventStream, where a repeat of 53 bits among a room's breaks is possible but not expected.
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:
| attribute | value |
|---|---|
ID |
quoted: the break's ID (§4.4) |
CLASS |
quoted: com.apple.hls.interstitial, the class Appendix D.2 defines |
START-DATE |
quoted: t0 + t of the opening cue, as YYYY-MM-DDTHH:MM:SS.mmmZ in UTC, the milliseconds floored (live-events §8.3) |
CUE |
quoted: ONCE for a mid-roll, POST,ONCE for a post-roll (§4.4.5.1) |
DURATION |
a break of type 4: its duration as decimal seconds with three digits after the point (30.000) |
PLANNED-DURATION |
an out with a planned duration: that duration, as DURATION |
X-ASSET-LIST |
quoted: the break's HLS asset-list URI (§7.2) |
X-RESUME-OFFSET |
a mid-roll of type 4: its duration, as DURATION |
X-PLAYOUT-LIMIT |
a break of type 4: its duration, as DURATION |
X-SNAP |
when snap is not 0: quoted, OUT, IN or OUT,IN by its bits |
X-RESTRICT |
when restrict is not 0: quoted, SKIP, JUMP or SKIP,JUMP by its bits |
X-SKIP-CONTROL-OFFSET |
when skip_after_s is not 65535: skip_after_s in decimal (Appendix D.3) |
ONCEkeeps a viewer who seeks back over a break in a window of the room from being shown it again;POSTplays the post-roll once the program has played to its end, which for a room is its playlist'sEXT-X-ENDLIST.X-RESUME-OFFSETequal to the break's duration returns the viewer to the program where the break ends on the room's timeline, whatever the fill's length, so the program a viewer returns to is the program after the in. A post-roll has nothing to resume, and noX-RESUME-OFFSET.- An out whose end is not yet known has no
DURATION,X-RESUME-OFFSETorX-PLAYOUT-LIMIT: the closing tag adds them (§5.2).
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:
| attribute | value |
|---|---|
ID |
quoted: the break's ID |
CLASS |
quoted: as §5.1 |
DURATION |
the break's duration (§4.3), as §5.1 writes durations |
X-RESUME-OFFSET |
the same value |
X-PLAYOUT-LIMIT |
the 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:
| attribute | value |
|---|---|
ID |
quoted: the break's ID followed by -preload |
CLASS |
quoted: com.apple.hls.preload, the class Appendix F defines |
START-DATE |
quoted: the interstitial's START-DATE less preload_ms, in its form |
DURATION |
preload_ms, as §5.1 writes durations |
X-URI |
quoted: the break's HLS asset-list URI (§7.2) |
X-TARGET-ID |
quoted: the break's ID |
X-TARGET-CLASS |
quoted: 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:
| attribute | value |
|---|---|
presentationTime |
pt0 + ⌊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) |
duration |
when the break's duration is known (a break of type 4, or an out its in has closed): that duration in milliseconds |
id |
the break's event id (§4.4) |
and its one child, ReplacePresentation for a mid-roll and InsertPresentation for a
post-roll:
| attribute | value |
|---|---|
uri |
the break's DASH asset-list URI (§7.3) |
earliestResolutionTimeOffset |
when preload_ms is not 0: preload_ms |
maxDuration |
when the duration is known: the duration in milliseconds |
executeOnce |
true, as HLS's ONCE |
skipAfter |
PT, 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 |
returnOffset |
a 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:
- 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>; - 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:
- its inputs are the viewer's pod, the policy's
slots, an avail and the funds remaining: the break's avail (§4.3) less the viewer's start offset (§7.4), or 0 when the offset reaches it; and the funds the decider states it had left of the decider grant's ceiling, at most that ceiling (§3.4); - the candidates are taken in order of
price_microsdescending, ties byad_idascending (as bytes); each is taken when fewer thanslotsare taken, itsduration_msis at most the avail not yet taken and itsprice_microsat most the funds not yet taken; the fill is the taken candidates in that order, slot 0 first.
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:
- form 1, stitched: an avail of an attested manifest, named by
avail_idandmanifest_hash, as SPEC §5 registers it. This revision changes nothing in it: its signed bytes, its identity and every receipt of it stay as they were; - form 2, break: a break of a room's schedule and the asset list one viewer was served for it, defined here.
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:
| member | value |
|---|---|
form |
2 |
room |
32 bytes: the room id |
cue |
32 bytes: the break's digest (§4.4) |
session |
16 bytes: the session (§2), one value for every break of one playback session, and no other's |
start_offset_ms |
the viewer's offset into the break (§7.4); 0 for a viewer present at its start |
format |
from 0 to 255: 1, the HLS asset list (§7.2); 2, the DASH list MPD (§7.3) |
asset_list_sha256 |
32 bytes: SHA-256 of the asset list served, its exact bytes |
avail_ms |
the break's avail (§4.3) |
slots |
the policy's slots |
funds_remaining_micros |
the funds the decision ran with (§7.1) |
pod_hash |
32 bytes: §8.3 |
impressions |
an 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_root |
32 bytes: §8.3 |
budget_spent_micros |
the billable spend (§8.3) |
work_decide_ops |
the candidates the decision considered |
joules_micro |
the decision's energy, or 0 unmetered |
energy_provenance |
its class's label (energy-measurement §2.7: ModelBased, Estimator, OnChipCounter, CalibratedInstrument), or null unmetered |
energy_uncertainty, energy_counter_filtering, energy_calibration_ref |
only 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_pubkey |
32 bytes: the decider's Ed25519 key |
signer_id |
a string: the decider's did:key or origin-bound id |
sig |
64 bytes: §8.4 |
version |
a receipt of version 2 alone: 2. A receipt with no version member is of version 1 |
consent |
version 2 only: the consent signal (§8.6) |
supply_chains |
version 2 only: the supply chains (§8.7), one for each impression, in the impressions' order, each null when the decider states none for it |
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.
A receipt of version 1 has none of version, consent and supply_chains, and one of version 2
has all three; their members are §8.6's and §8.7's, each of its type, or the receipt is refused
receipt_malformed.
8.3 Hashes
BLAKE3, with integers big-endian, as the stitched form computes them:
- the pod hash: over
wai:video-ssai-pod0x01, the candidates' count (u64), and for each candidate in the pod's order,ad_id's length (u32) and bytes,creative_hash,duration_ms(u64) andprice_micros(u64); - an impression's leaf: over
wai:video-ssai-imp0x01,slot(u64),ad_id's length (u32) and bytes,creative_hash,duration_ms(u64),price_micros(u64) andquartiles_reached(u8); - the Merkle root over the leaves in slot order: jwp-receipts §2.2's tree, each leaf taken as a content hash (its first leaf form) and the final one duplicated on an odd level; all zero for no impression;
- the billable spend: the sum of
price_microsover the impressions whosequartiles_reachedis at least 1.
8.4 Signing payload and identity
The signing payload is the concatenation of:
wai:video-ssai-break-receipt(ASCII) and 0x01;room,cue,session,start_offset_ms(u64),format(u8),asset_list_sha256,avail_ms(u64),slots(u64),funds_remaining_micros(u64);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;- the grant reference:
grant_hash, the length (u32) and bytes ofcapability,joule_ceiling_micro(u64), then 1 andfunds_ceiling(u64), or 0 for none; - the impressions' count (u64), then each one's leaf;
signer_pubkey, then the length (u32) and bytes ofsigner_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.
A receipt of version 2 signs, in place of item 1, wai:video-ssai-break-receipt (ASCII) and
0x02, and, between items 5 and 6, its consent signal and then its supply chains, each in its
signed encoding (§8.6, §8.7). No receipt of version 1 is one of version 2, and every receipt of
version 1 keeps its bytes and its identity.
8.5 Its own rules
A form-2 receipt holds, on its own:
formatis 1 or 2;- its signature verifies (
bad_key,bad_signature); merkle_rootis the root of its impressions,budget_spent_microstheir billable spend, and whengrant'sfunds_ceilingis notnull, the spend is within it (§3.4);- the impressions' slots are 0, 1, 2, … in order, each
quartiles_reachedis at most 5, andwork_decide_opsis at least their count; - the figure and its class cohere as energy-measurement §2 has them:
joules_micro0 with no class, or above 0 with a class, a hardware class's declared uncertainty withrelative_ppmandwindow_usabove 0 and at the resolution floor (joules_micro×relative_ppmat least 288 676), and a calibrated instrument's reference holding a character that is not Unicode White_Space; - of version 2, its consent signal holds the rules of §8.6, and its supply chains the rules of §8.7.
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.
Rule 6 that fails is consent_invalid for the consent signal and supply_chain_invalid for the
chains.
8.6 The consent signal
A receipt of version 2 states, in its member consent, the consent signal its decision was
made under:
| member | value |
|---|---|
gpp |
the Global Privacy Platform string (the GPP string) the player gave with its request for the asset list, byte for byte; "" when it gave none |
gpp_sid |
the sections of that string the player gave as applying to the request, each a section id from 1 to 65 535, in the order given; [] for none |
child_directed |
1 when the request is directed to children: the policy sets child (§3.1), or the player's request states so; 0 otherwise |
They are three of the members OpenRTB 2.6 gives an ad request's Regs object, gpp, gpp_sid
and coppa (its §3.2.3), and the GPP string is the one the Global Privacy Protocol String
defines. The signal holds these rules, or the receipt is refused consent_invalid (§8.5 rule 6):
gppis at most 65 535 bytes, each an ASCII letter or digit,-,_,.or~, the characters the GPP string's text lists as URL-safe; and when it is not empty, no part of it is empty between two of its~delimiters, before the first or after the last: the header and each section are there;gpp_sidholds at most 16 section ids, no two the same, each at least 1, and none whengppis empty;child_directedis 0 or 1.
Its signed encoding (§8.4), big-endian as every field of the receipt: the length (u32) and
bytes of gpp; the count (u64) of gpp_sid, then each section id (u16); child_directed
(u8).
What each party does with it:
- The player. A player that holds a GPP string for the viewer from its platform's consent
tool MUST give it, with the sections that apply, with its request for the asset list, as
it holds it, by a means this revision does not fix (for one, the request's parameters
gppandgpp_sid, as the GPP string's text names them for a service reached by a URL); a player whose request is directed to children states so the same way. A player that holds no string gives none. It MUST NOT write, alter or complete a GPP string: the GPP string's text has a property's owner or its consent tool make one, and every other party pass it on as it is. - The decider. A decider MUST state in the receipt the string and the sections it was
given, byte for byte, and MUST pass them unchanged in every request it makes to fill the
break (an OpenRTB 2.6 request's
Regs.gppandRegs.gpp_sid, or a URL'sgppandgpp_sid). It MUST setchild_directedwhen the policy sets child or the request states it, and MUST then mark every request it makes to fill the break as directed to children (an OpenRTB 2.6 request'sRegs.coppa1). It MUST NOT clear a flag the policy or the request set, and MUST NOT state a string or a section it was not given. - What no party infers. An empty
gppstates that no string was given: not that the viewer consented, and not that no law applies to the request. Achild_directedof 0 states that neither the policy nor the request marked the request as directed to children: not that the viewer is not a child. Neither the string nor the flag names the viewer, and a decider or a verifier MUST NOT use either to identify a viewer, or to link one viewer's receipts to one another. That rule binds those parties and does not keep receipts from linking: a GPP string that is not empty is made for the viewer by a consent tool, not for one request, and can stay the same, byte for byte, across the viewer's sessions and rooms, so whoever holds two receipts that carry it can link them through it (§12). - The verifier. A verifier reads the string's characters and its parts, and does not decode its sections: what a section states, and whether it permits what was done, is for the GPP string's texts and the parties to judge. A receipt that verifies shows what the decider signed that it was given and passed on; not that the viewer gave it, and not that any party honoured it.
8.7 The supply chain
A receipt of version 2 states, in its member supply_chains, the chain of sellers each of its
impressions was sold through: the SupplyChain object (OpenRTB 2.6 §3.2.25 and §3.2.26, and
the OpenRTB SupplyChain object) of the request whose answer gave the decider that
impression's candidate, with references to the files that let a buyer check each seller. A
pod's candidates can come from the answers to several requests, each request with its own
chain, so the member is an array with one entry for each impression, in the impressions' order,
and two impressions whose candidates answered one request state the same chain. WAI carries the object
and the references as those texts define them, and defines no supply-chain scheme of its own.
An entry is null when the decider states no chain for its impression, or an object:
| member | value |
|---|---|
ver |
the object's ver: its version, major.minor (1.0 in the revision read) |
complete |
its complete: 1 when the chain holds every node back to the owner of the inventory, 0 otherwise |
nodes |
its nodes, in its order: the first the initial seller (in a complete chain, the owner's own account), the last the one that sent the request; each an object of the members below |
authorised |
the authorised-sellers file the chain is checked against, {"file", "domain", "sha256"}, or null |
A node:
| member | value |
|---|---|
asi |
the canonical domain of the advertising system (the SSP, exchange or other system that bidders connect to) |
sid |
the seller's or reseller's account id in that system |
hp |
1 when the node is in the flow of payment for the inventory, 0 otherwise |
rid, name, domain |
the node's request id, the name and the business domain of the entity paid under sid, each null when the node has none |
sellers |
SHA-256 of the advertising system's sellers file, the file at https://<asi>/sellers.json (Sellers.json 1.0), its bytes as the decider held them when it made the decision; null when it held none |
In authorised: file is ads.txt for a site's authorised-sellers file (ads.txt
Specification 1.1 §3.1) or app-ads.txt for an app's (Authorized Sellers for Apps
(app-ads.txt) 1.0), at https://<domain>/ followed by file; domain is the host the file is
at, the site's or the one derived from the app's developer URL; and sha256 is SHA-256 of the
file's bytes as the decider held them. The object's ext members are not carried.
A domain name is 1 to 253 bytes: two labels or more separated by ., each 1 to 63 ASCII
letters, digits and -, neither beginning nor ending with -. The texts read have asi and a
node's domain be a bare domain, never a URL. Each chain holds rules 1 to 3, and the member
rule 4, or the receipt is refused supply_chain_invalid (§8.5 rule 6):
veris one or more digits,.and one or more digits, at most 15 bytes, andcompleteis 0 or 1;nodesholds 1 to 32 nodes; each node'sasiis a domain name, itssid1 to 255 bytes, itshp0 or 1, itsridandnamenullor 1 to 255 bytes, and itsdomainnullor a domain name;authorised, when it is notnull, has afileofads.txtorapp-ads.txtand adomainthat is a domain name;supply_chainsholds as many entries as the receipt holds impressions.
Its signed encoding (§8.4), big-endian: the entries' count (u64), then each entry: 0 for
null; or 1, then ver (its length, u32, and its bytes), complete (u8), the nodes' count
(u64), and for each node asi and sid (each its length, u32, and its bytes), hp (u8), each
of rid, name and domain as 0, or 1 and its length (u32) and bytes, and sellers as 0, or
1 and its 32 bytes; then authorised as 0, or 1, file and domain (each its length, u32, and
its bytes) and sha256.
The files. A verifier finds a file a chain names among the supply files it holds by its SHA-256 (§9.1), and reads it so:
- a sellers file reads when it is UTF-8 with no byte order mark at its start, and a JSON
text (RFC 8259) within the limits that RFC 8259 leaves a reader to set (its §4, §8.1, §8.2 and
§9), which are these: its arrays and objects nest at most 64 deep, the file's own object
counted as the first; its value is an object with a member
sellersthat is an array of objects, the sellers; each seller has a memberseller_idthat is a string, and when it has aseller_type, that is a string too; neither the file's object nor a seller has two members of one of these three names; and no member name of the file's object or of a seller, nor the string of aseller_idor aseller_type, holds a\uescape of a surrogate (U+D800 to U+DFFF) unless it is a high one followed at once by a low one, the pair that escapes one character (RFC 8259 §7). The rest of the file is read only by RFC 8259's grammar: a number is not converted to a value, so one of any size reads, and a string's escapes are not decoded, so one that escapes a lone surrogate reads; - an authorised-sellers file reads when it is UTF-8, and is read as ads.txt Specification
1.1 §3.2 to §3.5 describe it, by these rules: a line ends at LF, CR LF or CR; a
#and the rest of its line are a comment, and dropped; a line is trimmed of spaces and tabs, and passed over when it is then empty; a line whose part before its first=is not empty and holds no,, space or tab is a variable record, and passed over; any other line, with the part from its first;(its extension fields) dropped, is a data record when it is three or four fields separated by,, each not empty once trimmed of spaces and tabs: the advertising system's domain, the account id, the relationship and, optionally, a certification authority's id. Every other line is passed over, as the text has a consumer read liberally.
A node is listed in a sellers file that has a seller whose seller_id is its sid, byte for
byte; the first such seller is the node's seller there. A node is authorised by an
authorised-sellers file that has a data record whose first field is its asi, ASCII letters
compared without regard to case, whose second field is its sid, byte for byte, and whose
relationship, compared without regard to case, is DIRECT, or, for any node but the first of a
complete chain, RESELLER. The first node of a complete chain
is the owner's own account, which ads.txt Specification 1.1 §3.3 has the owner list as
DIRECT; an account another party controls to resell is RESELLER.
What each party does with it:
- The decider. For each impression, a decider MUST state the chain of the request whose
answer gave it the impression's candidate, node for node as that request carried it, and
nullonly when that request carried no chain or no request gave the candidate; and it MUST state as a node'ssellers, or asauthorised, only the digest of a file it held when it made the decision. It MUST NOT state a node the request did not carry, leave out one it did, or state for one impression the chain of another impression's request. - The verifier. A verifier that holds a file a chain names checks the chain against it (§9.3 step 14); a part whose file it does not hold stays unchecked, and the verification says so (§9.4). What it checks is the chain against the bytes the decider bound: whether those are the files the advertising systems and the owner publish now is for the parties, who fetch them, to judge.
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:
- policy: the room's ad policy;
- pt0: the presentation time of
t0, in milliseconds on the Period's timeline (§6.2); - receipts: form-2 receipts in JSON (§8.2), in a list, each with the pod its decision ran over, the candidates in the order the decider gave them;
- under a policy that requires presentation claims, the claims, creatives and issuers of ad-presentation §9.1;
- supply files: the sellers files and authorised-sellers files it holds, by their bytes, in any order; a receipt's chains name each by its SHA-256 (§8.7).
9.2 Steps
It runs these steps and stops at the first refusal, which it reports with the reason and where:
- The room (live-rooms §9): a refusal is the ads verifier's, at
roomwith 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. - The policy, at
policy: it reads (§3.3:truncated,malformed,unknown_version,ad_policy_malformed); itsroomis the room id (wrong_room); its signature verifies under the descriptor'sauthority(bad_key,bad_signature);decider_grantis 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). - 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.
- The renderings (§5, §6): the tags, the events and the asset-list URIs of every break. No refusal.
- The receipts, in the list's order, each by §9.3: the refusal, at
receiptand 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. - The outcome (§9.4).
9.3 Each receipt
For receipt k, with its pod, in this order:
- It reads as §8.2 has it:
receipt_malformed. - Its
roomis the room id:wrong_room. - Its own rules (§8.5):
bad_key,bad_signatureorreceipt_invalid. signer_pubkeyis the decider (§3.4):receipt_not_decider.grantis the decider grant's reference:grant_hashits digest,capabilitywai.live.room,joule_ceiling_microitsjoules_ceiling, andfunds_ceilingitsfunds_ceiling, ornullwhen that is 2^64 − 1 (live-rooms §4.5):receipt_grant_mismatch.cuenames a break of the schedule:receipt_break_unknown.- The decider grant is good at the break's
t(§3.4):outside_grant_window. pod_hashis the pod hash (§8.3) of the pod given:pod_mismatch.avail_msis the break's avail,slotsthe policy's, and the impressions, lessquartiles_reached, are the fill §7.1 gives for the pod, the slots, the avail lessstart_offset_msandfunds_remaining_micros:decision_mismatch.asset_list_sha256is SHA-256 of the asset list of the fill in the receipt'sformat(§7.2, §7.3):asset_list_mismatch.- No receipt before it in the list names this
cueand thissession:receipt_duplicate. A viewer is filled once for a break. - Spend-equivalence.
funds_remaining_microsis within the decider grant's ceiling, and so is the spend that counts of this receipt and every receipt before it, together (§3.4): otherwisefunds_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.
- The consent signal, under a policy of version 3: the receipt is of version 2
(
request_terms_absent); and when the policy sets child, itschild_directedis 1 (child_directed_unset). - The supply chains, of a receipt of version 2: for each chain it states, in the
impressions' order, and in it each node in order whose sellers file the verifier holds, that
file reads (
supply_file_malformed) and lists the node (supply_seller_unlisted); then, when the verifier holds the authorised-sellers file the chain names, that file reads (supply_file_malformed) and authorises every node, in order (supply_seller_unauthorised) (§8.7). A chain two impressions state is checked for each.
Steps 13 and 14 read what the receipt signed and the bytes of the files held; neither changes the decision, the asset list or the spend.
9.4 Outcomes and what a verification states
- refuse: the reason and where (§9.2).
- gap: the room has a gap: its gaps, under live-events' code
delivery_gap. - accept: no refusal and no gap.
With accept, the verifier states:
- each break, in schedule order: its digest, ID and event id, its author,
cue_id,tand type (out or break), its placement, its planned duration and avail, and, once closed by an in, the in's digest, its end and its duration; or that it is open; - each chapter: its digest,
tandduration_ms; - the HLS tags, in the order of §5.4; each break's HLS and DASH asset-list URIs; the DASH event streams (§6.3), and each out's event as it was written before its in;
- each receipt: its identity (§8.4), the asset list it binds, written by the verifier, and its billable spend; and the spend of every receipt together, the spend that counts (§9.3 step 12);
- under a policy that requires presentation claims, what ad-presentation §9.4 states;
- each receipt of version 2: its consent signal, as §8.6 writes it; and its supply chains, one
for each impression in its order, each
nullwhen it states none, or: itscomplete, and for each node itsasiandsid, whether the verifier held its sellers file (sellers_checked), theseller_typeof its seller there (§8.7), as written, ornull, and, when the authorised-sellers file was held, the relationship of the first record that authorises it, in capitals, ornull; and whether that file was held (authorised_checked).
10. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised 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 |
consent_invalid |
§8.5 rule 6, §8.6, §9.3 step 3 |
supply_chain_invalid |
§8.5 rule 6, §8.7, §9.3 step 3 |
request_terms_absent |
§9.3 step 13 |
child_directed_unset |
§9.3 step 13 |
supply_file_malformed |
§8.7, §9.3 step 14 |
supply_seller_unlisted |
§8.7, §9.3 step 14 |
supply_seller_unauthorised |
§8.7, §9.3 step 14 |
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:
- Rooms, each a room (descriptor, grants, key-log head or none, chains), its policy,
pt0and its receipts with their pods, and the outcome §9.4 gives them with everything it states:- a live break, an out with no planned duration and its in, a viewer present at its start filled from an HLS asset list, and a viewer who joins it running, filled from a DASH list MPD with what remains of it;
- a mid-roll of known duration under a policy that restricts skipping and jumping, snaps both ways and offers a skip control, with a viewer whose pod nothing fits;
- a program with chapters, a mid-roll, and a post-roll at its close;
- chapters, and an out with a planned duration closed by its in;
- one refused room for each reason of §10, each a control of one defect; one for each
other rule of §8.5, each refused
receipt_invalid; a cue by a principal whose grant does not permitcue, which the room refuseskind_not_permitted; and a decider grant revoked through the key log, and one whose window ends before the break; - a room with a gap;
- the request terms (§8.6, §8.7): the live break under a policy of version 3, its first viewer's receipt stating a GPP string with its applicable section and, for its two ads, the chains of the two requests they answered, one from the channel's own account at an SSP through an exchange and one from the channel's own account at the exchange, each checked against the sellers files and the channel's app-ads.txt, and its second viewer's stating no string, a child-directed request and an incomplete chain whose sellers file the verifier does not hold; a room the policy marks as directed to children; one chain two impressions state, of four nodes that the channel's ads.txt authorises only when it is read by each rule of §8.7 (a record of four fields, other letter cases, CR LF and a CR alone, an extension field); and a sellers file that lists its node only when what §8.7 does not read is read by RFC 8259's grammar alone (numbers past binary64's range and of 400 digits, escapes of lone surrogates in members skipped, a member 64 deep);
- refused there: four receipts whose GPP string, child-directed flag, a node's seller id or
the order of its chains was changed after the decider signed them, each
bad_signature; a receipt stating version 3, one of version 1 carrying a consent signal, and one of version 2 without its chains (receipt_malformed); a receipt for each rule of §8.6 (consent_invalid) and for each rule of §8.7 (supply_chain_invalid); a receipt of version 1 under a policy of version 3 (request_terms_absent) and one with no child-directed flag in a room directed to children (child_directed_unset); a sellers file with no sellers, one nested 65 deep, one whose second seller's id escapes a lone surrogate, one withsellerstwice and one holdingNaN, and an app-ads.txt that is not UTF-8 (eachsupply_file_malformed); a node its sellers file does not list (supply_seller_unlisted); a first node of a complete chain authorised only as a reseller, a node the app-ads.txt does not name, and one it names only in lines a reader passes over, a comment and a line of five fields (eachsupply_seller_unauthorised); and a policy whoseaudiencesets a bit above bit 0 (ad_policy_malformed).
The sellers files and the authorised-sellers files the vectors hold are files of their own,
supply.NAME, each named in the supply_files of the vectors that hold it, read as bytes;
every one is a fixture written for its case under .example names.
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; it reads every
receipt's request terms and every sellers file and authorised-sellers file by §8.6 and §8.7
itself; and it mutates copies of the corpus and requires every copy to fail.
12. Security and privacy considerations
- Every break is a cue its author signed, under a grant that permits
cue; a relay or an edge can withhold a cue, which is a gap in its author's chain, but cannot add or move a break. The date ranges and events an edge writes are recomputable from the signed records, and a playlist whose interstitials differ from them is one the edge altered. - The decision is a pure function of its inputs, and every input is bound: the pod by its hash, the avail by the break, the slots by the policy, the funds by the receipt, and the asset list served by its digest, whose URIs name each creative by its hash. A decider that serves an ad the decision did not select, or a creative other than the one it bills, signs a receipt that fails §9.3.
- The funds a decision states are the decider's statement. What is checked is the bound: no receipt spends past the ceiling, and no receipts together do, so an overspent room fails verification at the receipt that crosses the ceiling.
quartiles_reachedis the decider's statement of the beacons it received; a receipt binds it and its billing, not the beacons. The impressions a buyer and a seller reconcile are the same signed bytes. A policy of version 2 can require more: that an impression count only when the player's signed presentation claim verifies, reaching the quartile the receipt states under a viewability rule, optionally with a device attestation (ad-presentation §9, §13 for what that does and does not prove).- A receipt names a session by 16 bytes the decider assigns. A decider SHOULD assign a value with no meaning outside its own records, and MUST NOT derive it from a viewer's key or another identifier a third party holds, so that receipts that carry no GPP string (§8.6) do not link a viewer across rooms; within a room, one session's receipts are linkable to one another by design, so that a viewer is filled once for a break (§9.3 step 11). Under a policy that requires presentation claims the session is derived from a key the player makes for that session of that room alone (ad-presentation §3), which is no viewer's key and links nothing across rooms.
- The asset-list request carries the viewer's playback-session parameter (Appendix D.4) to the decider, and nowhere else: the manifests every viewer shares carry nothing of any viewer.
- The policy is signed by the room authority and the receipts by the decider. A decider whose grant is revoked or out of its window has its receipts refused (§9.2, §9.3).
- The consent signal is bound into what the decider signs: a receipt whose string, sections or child-directed flag changed after signing fails its signature (§8.5 rule 2), as one whose chains changed does. A verified receipt shows what §8.6 says it shows, what the decider stated it was given and passed on, and no more. The string travels with the asset-list request to the decider, from the decider to those it requests ads from, and in the receipt to whoever holds it.
- A GPP string links what carries it. A consent tool makes the string for a viewer, not for
one request, and it can stay the same, byte for byte, across the viewer's sessions and rooms.
A receipt whose
gppis not empty can therefore be linked, through the string, to every other receipt that carries the same string, in any room, and to a stitched pod-input record that carries it (stitched-presentation §4.7), by whoever holds them: the same viewer's among them, for as long as the string stays the same. §8.6's rule that a decider and a verifier not use the string to link receipts binds those parties; it does not remove the link. The string is in the signed bytes, so a holder cannot strip it from a receipt and keep the receipt verifiable. What the session item above says of rooms holds of a receipt whosegppis empty. - A room for children. A policy that sets child binds every decision of the room to a child-directed signal: a receipt that does not carry one is refused (§9.3 step 13). That the decider marked each of its requests so is its statement, as the funds a decision states are.
- The supply chain and the digests of the files it names are the decider's signed statement. A verifier that holds the files checks every node against them; each file is fetched from the domain it belongs to, by its own text's access method, and a verifier that needs them to be the files published now fetches them itself and compares their digests with the receipt's.