open-standards/wai/extensions/receipt-ledger.md
Transaction Science ba9c439cf9 receipt-ledger §5.2: one registry of record kinds; the job kinds are 14 to 16
Kinds 10 to 13 were already revenue-share §7.4's run and invoice and
opportunity-report §7.3's cue event and room policy; the job source's OER/2
receipt, int_motion decode receipt and route record took 10 to 12 and so meant
two things. They are now 14, 15 and 16. §5.2's table registers every kind a
WAI text names a record by, 10 to 13 pointing to the texts that define them,
unchanged, and those texts say so. Kinds 128 to 255 are reserved for private
and provisional use: a WAI record never states one, and an invoice that lists
one is refused private_kind, in either version. A version-2 invoice lists kinds
1 to 9 and 14 to 16, never another text's. Three controls; the corpus and both
implementations follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:51:06 -04:00

44 KiB
Raw Permalink Blame History

WAI Extension: Receipt Ledger

Status: Draft. Registers the record wai.delivery.ledger: invoice lines a customer can recompute from the signed records WAI already defines. A seller and a buyer both sign a tariff, WLT1: one currency, and one price for each unit those records state, the units of a room session (live-metering §4.4), the acknowledged bytes of each object class (settlement §3.3), the packages, entries and stored bytes of a package receipt (packaging §9), the counted impressions of a wai.video.ssai receipt of the break form (live-ads §8, ad-presentation §9.2) and the presentations of ads another party stitched (stitched-presentation §7.4); and the scope it covers, the rooms, the delivering parties, the packagers, and the ad deciders and stitchers whose records are billed under it. The ledger verifies each source by the extension that defines it, and the seller signs an invoice, WLI1, for a period: the records it was computed from, one line per source, unit and price, each line citing the records it came from, and the joules those records state, per scope and per acquisition class with their largest declared uncertainty, never summed across classes or scopes. Invoices under one tariff form a chain, and no record is billed in two of them. A verifier given the tariff, the invoice, the invoice before it and the sources recomputes every line and refuses an invoice that does not recompute. Beside the invoice, the ledger states two reports: the ad decisions reconciled against the players' presentation claims (for a stitched stream, stitched-presentation §7.5's own statement), and the delivery of each party under the one tariff with the bytes two parties were acknowledged for twice. A tariff of version 2 also prices media jobs (§4.5): a job is billed from its route record (route-record), its hops' OER/2 receipts and WAI's receipt of its output, in the units those receipts state. No new transport, no payment and no tax: an invoice is the recomputable figures a payment rests on. The reference implementation is the receipt_ledger module of wai-rs (feature receipt_ledger) and the service wai/wai-ledger; the corpus is live-conformance/ledger/, and its second implementation is live-conformance/receipt_ledger_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: the WAI texts this one cites; ISO 4217 (the alphabetic currency code); RFC 8032; RFC 8259; FIPS 180-4.

1. Scope

In scope: how one tariff prices rooms, packages, HTTP and relayed delivery and ads together (§3); the tariff, its two signatures and its scope (§4); the sources a ledger is given and what each contributes (§5); the invoice, its records, lines, citations and energy, its signature and its chain (§6); verification (§7); the two reports (§8); and the reference service (§9).

Out of scope: a transport; payment, a payment rail, tax and any contact with a party outside the records; how a seller chooses its prices; how a party measures its energy, which energy-measurement governs; and the records themselves, which their own extensions define, verify and leave as they are. The live-metering tariff and the settlement statement's tariff keep their meaning: this extension reads them, and changes neither.

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

2. Terms

  • Seller, buyer: Ed25519 keys (RFC 8032). The seller operates the ledger and signs its invoices; the buyer is the customer billed.
  • Time: microseconds since 1970-01-01T00:00:00Z, the unit of a room's t0 (live-rooms §3.1). A period is a span of time from its start (included) to its end (excluded).
  • Minor unit: a currency's smallest unit (ISO 4217); an amount is an integer of millionths of one, as settlement §7.3's amounts are.
  • Source: one record a ledger bills from, with what its own verifier is given (§5).
  • Record identity: the 32 bytes a record of a kind is named by (§5.2).
  • u64, u32, u16, u8: little-endian unsigned integers of that many bits. A digest is SHA-256 (FIPS 180-4), 32 bytes; zero of a key or digest field is 32 zero bytes.
  • Class, tag, unmetered marker: an acquisition class of energy-measurement §2.7, its tag byte (2 ModelBased, 3 Estimator, 4 OnChipCounter, 5 CalibratedInstrument), and a figure of 0 with no class.

3. One tariff across rooms, packages and HTTP delivery

The records a deployment bills from state their units in their own terms. A room-session receipt states microseconds and events (live-metering §4.4), and may state a tariff of its own, which prices the session's grants against their funds ceilings (§4.6 there). A settlement statement states acknowledged bytes by object class, whatever carried them: a relay over MOQT, or a cache over HTTP whose deliveries were acknowledged as settlement §9.1 maps them; it may state a tariff too, which prices its own lines (§7.3 there). A package receipt states entries and bytes (packaging §9) and no tariff. An ad decision's receipt states impressions (live-ads §8.2) and no tariff of the seller's; so does a stream another party stitched, whose stitcher signs a pod-input record, WSI1, of the ads it chose and whose players sign a claim, WSC1, of each ad presented (stitched-presentation §4, §5).

A tariff of this extension prices every one of those units in one currency, so one invoice bills a buyer's rooms, deliveries, packages and ads together. Three rules keep every figure in it consistent with what the records say:

  1. A record that states a tariff states this one. A room-session receipt of a source either states no tariff (currency zero) or the tariff's currency and its thirteen room prices, in live-metering §4.1's order; a settlement statement either states no tariff or the tariff's currency and its four delivery prices, in settlement §7.1's order. A record under another tariff is refused tariff_conflict (§7.2 step 5): the funds its grants were held to, or the amount its party stated, were computed at other prices, and an invoice at these prices would contradict it.
  2. One rounding, the same everywhere. Every amount is a quantity times a price, divided by the unit's divisor and rounded down once per invoice line (§6.4), as settlement §7.3 rounds a statement's line. A statement's own amount rounds once per payer; the invoice rounds once per party and class, so the two can differ by less than one millionth of a minor unit per payer line, and the delivery report (§8.2) states both.
  3. Units are never converted. A room is billed in its own units, a delivery in bytes, a package in packages, entries and stored bytes, an ad in impressions, a job in jobs or in the subpixels its output's receipt states (§4.5). No line turns one unit into another, and no line is priced from joules: the energy of an invoice is stated beside its lines (§6.6), per class, and is not an amount.

So the tariff is the one place prices live: a room receipt's funds verdicts, a statement's amount and an invoice's lines all follow from it, and a buyer who holds the tariff and the records recomputes all three.

4. The tariff

4.1 Layout

field size meaning
magic 4 WLT1 (ASCII)
version 1 1, or 2 for a tariff that prices jobs (§4.5)
seller 32 the seller's key, which signs the tariff and every invoice under it
buyer 32 the buyer's key, which accepts the tariff by signing it
start, end 8 each u64: the period the tariff is good for
currency 3 three capitals A–Z (ISO 4217)
room 52 thirteen u32: millionths of a minor unit per second of publisher_us, of subscriber_us for each layer class 0 to 8, of composition_us and of recorded_us, and per event sent (live-metering §4.1's order)
delivery 32 four u64: millionths of a minor unit per 10^9 acknowledged bytes of each object class, in class order (settlement §7.1)
package 24 three u64: millionths of a minor unit per package receipt, per entry, and per 10^9 stored bytes
ads 16 two u64: millionths of a minor unit per presented impression, and per billable impression
job 2 + Σ (18 + c) version 2 only, absent in version 1: a u16 count j, then each job price (§4.5): cap_len (u8), capability (cap_len bytes), unit (u8), divisor (u64) and price (u64)
scope 2 + 33 × n a u16 count n, then each entry: kind (u8) and id (32), in strictly ascending order of (kind, id)
seller_sig 64 §4.2
buyer_sig 64 §4.2

4.2 Digest and signatures

The digest of a tariff is SHA-256 over its bytes before seller_sig, the body. Its seller_sig is Ed25519 by seller over the 18 bytes wai:ledger-tariff (ASCII) and 0x01, followed by the body; its buyer_sig is Ed25519 by buyer over the 18 bytes wai:ledger-accept (ASCII) and 0x01, followed by the body. Both are verified strictly, as SPEC §7 Signatures requires: a key that fails step 1 or 2 is bad_key, and a signature that fails a later step is bad_signature. The buyer's signature is what binds the buyer to the prices: a tariff the buyer did not sign bills nobody.

4.3 Reading and rules

A reader reads exactly the bytes it is given as one tariff, in the order of §4.1, and refuses at the first of these that holds: fewer than 4 bytes, truncated; a magic other than WLT1, malformed; no fifth byte, truncated; a version other than 1 and 2, unknown_version; the bytes end before a field the layout requires, truncated; bytes after buyer_sig, malformed. It then refuses as tariff_invalid a tariff that breaks any of these:

  • start is below end; currency is three capitals A–Z;
  • seller and buyer are not zero, and not the same key;
  • scope has at least one entry, in strictly ascending order of (kind, id), each kind 1 to 4 (1 to 5 in version 2) and each id not zero;
  • in version 2, job has at most 255 prices, each as §4.5 requires, in strictly ascending order of (capability's bytes, unit).

Then the seller's signature, then the buyer's: bad_key, bad_signature.

4.4 Scope

A scope entry names whose records the tariff bills:

kind id a source is in scope when
1 a room id a room-session receipt's room is the id
2 a party's key a settlement statement's party is the id
3 a packager's key a package receipt's signer_pubkey is the id
4 an ad decider's or a stitcher's key every receipt of an ads source has signer_pubkey the id (live-ads §3.4); a stitched source's records are accepted only when their signer is such an id: the verifier's signers (stitched-presentation §7.1) are the scope's entries of kind 4
5 a router's key (version 2) a job source's route record has signer the id (route-record §5.1)

The scope is what the buyer agreed to pay for: a record signed by a party the tariff does not name is refused outside_scope (§7.2 step 5), whoever submits it.

4.5 Job prices (version 2)

A tariff of version 2 prices the media jobs a router routed and a hop served. A job price is a capability (route-record §2: 1 to 128 bytes, lowercase ASCII letters, digits, _ and ., beginning wai.), a unit, a divisor of at least 1 and a price: millionths of a minor unit per divisor units of the unit, for a job whose serving hop served that capability. The units are the records' own figures, never converted:

unit name a job's quantity
1 jobs 1
2 subpixels the sum of its output receipts' work_pixel_ops (integer-payloads §15.1): the subpixels its output decodes to

A job the tariff has no price for, by its serving capability, is billed nothing, and a buyer pays for a capability only at a price it signed. A package job stays a package source (§5.1), billed by §4.1's package prices.

5. Sources

5.1 Kinds and their documents

A source is a JSON object (RFC 8259) whose kind names its extension, holding exactly what that extension's verifier is given. Bytes are lowercase hexadecimal strings.

kind members verified as
room room (descriptor, grants, key_log_head (a string or null), chains, a list of lists of records), receipt, hops (each record and ledger), descriptors, compositions, takes, logs (a string or null) live-metering §5.1, its receipt being receipt
delivery statement, records (each record and its items, one item list per entry), proofs, hops, rooms (each descriptor and grants) settlement §8.2
package receipt (the text of package.receipt.json), indexes (the index files in the receipt's component order) packaging §9, a receipt of version 2 against its indexes
ads descriptor, grants, key_log, chains, policy, pt0_ms, receipts (form-2 receipts in their JSON carriage), pods, claims, creatives, issuers live-ads §9, with ad-presentation §9 under a policy that requires claims
stitched records (pod-input records), claims (stitched claims), manifests, responses (VAST and VMAP documents), receipts (form-1 receipts in their JSON carriage), creatives, issuers stitched-presentation §7, its signers the tariff's (§4.4)
job (version 2) request, pricebook, policy, record, receipts, keys (a route record's verification document, route-record §7.1, its receipts and keys not null) and outputs, a list of objects of exactly receipt (the text of a wai.video.int_motion decode receipt's JSON form, integer-payloads §15.2) and codestream (the WIV1 payload it describes) route-record §7, accepted with its hop receipts checked; then each output receipt read by integer-payloads §15.2 and re-derived from its codestream (§15.1 there), or output_invalid at outputs and its index

A source is accepted when its extension's verifier accepts it; for ads, when the outcome is accept (live-ads §9.4), a gap being source_gap. A stitched source is always accepted, since its verification refuses nothing as a whole (stitched-presentation §7.5): a record it does not accept, of a signer outside the scope among them, bills nothing, and a claim it does not find presented counts nothing. A source that is not such an object, whose kind is none of these, or whose members are not exactly its kind's, each of its form, is refused malformed. Under a tariff of version 1, a job source is not a kind, and is malformed.

A job source's billed hop is the attempt of its route record that is billed (route-record §5.2), if any; its serving capability is that attempt's candidate's capability, and its figure is the figure of that attempt's OER/2 receipt: the unmetered marker (oer-receipts §3.1), or energy.joules_total in microjoules, rounded to the nearest and a half to the even one, with the receipt's energy.class. A billed hop's receipt whose figure has no class, or rounds below 1 µJ or past what a u64 holds, cannot be stated in a row (§6.6): the source is refused job_figure_invalid, at receipts and the receipt's index.

5.2 What a source contributes

Each source contributes records, each named by a kind and an identity. This table is the one registry of record kinds: every WAI text that names a record by a kind (in a citation digest, or in a list of records) takes the kind from here, and a new kind is assigned here, the next free one. Kinds 10 to 13 are defined by the texts that cite them and are no ledger source's; the others are the ledger's own:

record kind record identity contributed by
1 a room-session receipt, WMR1 its digest (live-metering §4.2) a room source: its receipt
2 a per-hop record, WMH1 its digest (live-metering §3.5) a room source: each record its receipt names; a delivery source: each record its statement names in hops
3 a settlement statement, WSS1 its digest (settlement §7.4) a delivery source: its statement
4 an acknowledgement or declaration, WDA1 its digest (settlement §3.5) a delivery source: each record its statement names in records
5 a package receipt its receipt_hash (packaging §9) a package source
6 a wai.video.ssai receipt of the break form its identity (live-ads §8.4) an ads source: each receipt
7 a presentation claim, WPC1 its digest (ad-presentation §5.3) an ads source: each claim that reads
8 a pod-input record, WSI1 its digest (stitched-presentation §4.4) a stitched source: each record it accepts
9 a stitched claim, WSC1 its digest (stitched-presentation §5.3) a stitched source: each claim that reads
10 a run of a channel's program-as-run its identity (revenue-share §7.4) no source: cited by revenue-share §7.4 and opportunity-report §7.3
11 an invoice, WLI1 its digest (§6.7, as revenue-share §7.4 cites it) no source: cited by revenue-share §7.4
12 a cue event its event digest (opportunity-report §7.3, live-events §3.2) no source: cited by opportunity-report §7.3
13 a room policy its digest (opportunity-report §7.3, live-ads §3.2) no source: cited by opportunity-report §7.3
14 an OER/2 receipt the 32 bytes its receipt_id names (oer-receipts §5) a job source (version 2): the receipt of each attempt of its record
15 a wai.video.int_motion decode receipt its receipt hash (integer-payloads §15.1) a job source (version 2): each output receipt
16 a route record its digest (route-record §5.3) a job source (version 2): its record

Kinds 17 to 127 are unassigned. Kinds 128 to 255 are reserved for private and provisional use: an implementation's own records, such as a hosted service's provisional records carried beside WAI's until WAI defines them, may name records by them, and no kind WAI assigns later will ever be one of them. A WAI record never states one, and a WAI verifier refuses one wherever a WAI record states a kind, private_kind: in this extension, an invoice's records (§6.8). No citation digest WAI defines covers one.

A source's head is its receipt, statement or package receipt; an ads source's heads are its receipts, a stitched source's its accepted records, and a job source's its route record. A head of one source that another source of the invoice also contributes is refused record_duplicate; a per-hop record, which a room receipt and a statement can both name, is contributed once. An OER/2 receipt or an output receipt a job source contributes that an earlier job source of the invoice also contributes is refused record_duplicate too: one hop serves one job, and one output is one job's, so neither is billed for two.

5.3 Placement in time

A source is in a period when:

  • room: the receipt's session, placed by its room's t0, lies within it: t0 + start ≥ the period's start and t0 + end ≤ its end;
  • delivery: the statement's period lies within it;
  • ads: the t of the break each receipt names, placed by the room's t0, is within it;
  • package and stitched: always: a package receipt and a pod-input record state no time, and the chain (§6.9) bills each once;
  • job: its route record's accepted is within it.

Two room receipts of one room whose sessions overlap, two statements of one party whose periods overlap, two receipts of the invoice's ads sources naming one break and one session, and two route records of one router naming one job, bill one thing twice: the later is refused record_overlap.

6. The invoice

6.1 Layout

field size meaning
magic 4 WLI1 (ASCII)
version 1 its tariff's version, 1 or 2
seller, buyer 32 each the tariff's
tariff 32 the tariff's digest
prev 32 the digest of the invoice before it in its chain (§6.9); zero for the first
start, end 8 each u64: the period it bills
currency 3 the tariff's
records 4 + 33 × r a u32 count, then each record (§5.2): kind (u8) and id (32), in strictly ascending order of (kind, id)
lines 2 + 94 × l a u16 count, then each line (§6.3), in strictly ascending order of (source_kind, source, unit)
energy 1 + 18 × e a u8 count, then each row (§6.6): scope (u8), tag (u8), figures (u32), joules (u64, µJ), max_ppm (u32), in strictly ascending order of (scope, tag)
unmetered 16 or 20 four u32: the unmetered figures of scopes 1 to 4 (§6.6); in version 2, five, scopes 1 to 5
amount 8 u64: the sum of the lines' amounts
sig 64 §6.7

6.2 Records

records lists every record the sources contribute (§5.2), each once. It is what the invoice was computed from, and what the next invoice of the chain may not bill again.

6.3 Lines

A line is 94 bytes:

field size meaning
source_kind 1 1 room, 2 delivery, 3 package, 4 ads, 5 job (version 2)
source 32 the room id, the delivering party, the packager, the ad decider or the router
unit 1 the unit (below)
quantity 8 u64: the quantity of the unit the records state
price 8 u64: the tariff's price for the unit
amount 8 u64: §6.4
cited 4 u32: the records the line cites (§6.5)
cites 32 their citation digest (§6.5)

The units, the quantity each sums over the sources of its source, and the price and divisor of each:

source_kind unit name quantity price divisor
1 1 publisher_us the receipts' publisher_us room 0 10^6
1 2–10 subscriber_us_0–subscriber_us_8 their subscriber_us of layer class unit − 2 room 1–9 10^6
1 11 composition_us their composition_us room 10 10^6
1 12 recorded_us their recorded_us room 11 10^6
1 13 events_sent their events_sent room 12 1
2 1–4 live, fetch, fill, ancillary the bytes of the statement's delivery lines of that class that are not free (settlement §7.2) delivery unit − 1 10^9
3 1 packages the number of package receipts package 0 1
3 2 entries their total_entries package 1 1
3 3 stored_bytes their stored_bytes package 2 10^9
4 1 presented_impressions under a policy that requires claims, the impressions that count (ad-presentation §9.2); of a stitched source, the presentations of its accepted records' ads (stitched-presentation §7.4), one per session ads 0 1
4 2 billable_impressions under a policy that does not, the billable impressions (quartile at least 1) ads 1 1
5 k the k-th job price's unit over the router's job sources with a billed hop whose serving capability is that price's capability: one per job (jobs), or their output receipts' work_pixel_ops (subpixels) the k-th job price's price its divisor

There is one line for each source and unit whose quantity is above 0, a price of 0 included, and none for a quantity of 0. A delivery line's bytes are those its party's downstream parties signed for (settlement §3.7): a declared byte and a free retrieval are never billed, and the delivery report states both (§8.2). An ad line counts impressions under one rule or the other, never both: a policy that requires claims bills only what the players' own claims show presented, and one that does not bills on the decider's beacons, under a unit of its own.

6.4 Amounts

A line's amount is ⌊quantity × price / divisor⌋, computed in integers wide enough not to overflow, and the invoice's amount is the sum of its lines' amounts. A line amount or a sum past what a u64 holds cannot be stated, and a ledger refuses to compute such an invoice.

6.5 Citations

Each line cites the records it came from:

  • a room line: the receipts of its room whose figure of the unit is above 0;
  • a delivery line: the statements of its party with a delivery line of the class that is not free, and each acknowledgement (mode 1) they name with an entry of the class that is not free (a retrieval class with a zero grant is free);
  • a package line: the package receipts of its packager whose figure of the unit is above 0;
  • an ad line: the receipts of its decider with at least one impression counted under the unit, and, for presented_impressions, each counted impression's own claim; of a stitched source, its stitcher's accepted records, and each presented own claim of their ads;
  • a job line: for each job it counts, its route record and its billed hop's OER/2 receipt, and, for subpixels, each of its output receipts whose work_pixel_ops is above 0.

cited is their number, and cites is SHA-256 over the 17 bytes wai:ledger-cites (ASCII) and 0x01, followed by each cited record's kind and identity, in ascending order of (kind, identity). Every cited record is in records. A buyer recomputes a line from the records it cites alone, and shows one line to an auditor with its records and nothing else.

6.6 Energy

Each record that states an energy figure states it once, in one scope:

scope figures
1 room_hops the per-hop records named by the room receipts (each restated by its receipt, live-metering §4.5)
2 delivery_hops the per-hop records named by the statements and by no room receipt of the invoice
3 packaging the package receipts' joules_micro and class
4 ad_decisions the ad receipts' joules_micro and class; a pod-input record states no figure
5 jobs (version 2) each job source's billed hop's figure (§5.1): the hop that served the job, in the class its OER/2 receipt states; a job with no billed hop states none

For each scope, unmetered counts its figures that are the unmetered marker, which are in no row and MUST NOT be presented as zero joules. For each scope and each tag among its metered figures there is one row: figures, their number; joules, their sum; and max_ppm, the largest relative_ppm among them for tags 4 and 5, which carry a declared uncertainty, and 0 for tags 2 and 3, which do not. When each figure's error is within its declared relative uncertainty, the sum's is within max_ppm of the sum. A row never sums figures of two classes, and no row sums two scopes: a relay's counter reading, a packager's model and a decider's estimate are three rows, since a sum of a model and a counter reading is neither. A row whose tag is 0 is refused classes_summed, and so is a row whose joules is the sum of the figures of its class and of at least one other class of its scope, while not the sum of its own (live-metering §4.5's rule). A scope's energy is whole when its unmetered is 0 and it has a row, partial when it has an unmetered figure, and none when it has no figure; a partial total MUST NOT be presented as the scope's energy.

6.7 Digest and signature

The digest of an invoice is SHA-256 over its bytes before sig. Its signature is Ed25519 by seller over the 19 bytes wai:ledger-invoice (ASCII) and 0x01, followed by the bytes the digest covers, verified strictly (SPEC §7 Signatures).

6.8 Reading and rules

A reader reads exactly the bytes it is given as one invoice, as §4.3 reads a tariff with WLI1 for WLT1 (truncated, malformed, unknown_version), and refuses as classes_summed an invoice with a row of tag 0. It refuses as private_kind an invoice with a record of a kind 128 to 255 (§5.2), and as invoice_invalid one that breaks any of these:

  • start is below end; currency is three capitals A–Z;
  • each list is in the strictly ascending order §6.1 gives it; each record's kind is 1 to 9 (in version 2, also 14 to 16: kinds 10 to 13 are no ledger source's, §5.2);
  • each line's source_kind is 1 to 4 (1 to 5 in version 2), its unit a unit of its kind (§6.3; for kind 5, at least 1), its quantity and cited above 0;
  • each row's scope is 1 to 4 (1 to 5 in version 2), its tag 2 to 5 and its figures above 0.

Then the signature (§6.7): bad_key, bad_signature.

6.9 Chains

The invoices of one tariff form a chain: each names in prev the invoice before it, the first names zero, and their periods do not overlap. A ledger keeps one chain per tariff. No record is in the records of two invoices of a chain: a record billed once is billed never again, whatever period a later invoice states, and a package receipt, which states no time, is billed in the invoice that first holds it.

7. Verification

7.1 Inputs

A verifier is given the tariff, the invoice, the invoice its prev names (the previous, or none), and the sources, a list. The ledger's verification document holds them as one JSON object: tariff and invoice (hexadecimal), previous (hexadecimal or null) and sources (§5.1).

7.2 Steps

It runs these steps and stops at the first refusal, which it reports with the reason and where:

  1. The tariff (§4.3), at tariff, or tariff and seller or buyer for a signature: truncated, malformed, unknown_version, tariff_invalid, bad_key, bad_signature.
  2. The invoice (§6.8), at invoice: truncated, malformed, unknown_version, classes_summed, private_kind, invoice_invalid, bad_key, bad_signature.
  3. The binding, at invoice: its tariff is the tariff's digest, and its version, seller, buyer and currency the tariff's: otherwise tariff_mismatch; its period lies within the tariff's: otherwise outside_tariff.
  4. The previous, at previous, when prev is not zero: an invoice whose digest is prev is given (record_missing); it reads and its signature verifies (§6.8, with those reasons); its tariff, seller and buyer are the invoice's (tariff_mismatch); and its end is at most the invoice's start (invoice_overlap).
  5. Each source, in order, at source and its index: it is verified as §5.1 says, a refusal being that extension's reason with that extension's place after the index (an ads gap is source_gap, and a source that does not read malformed); then it is in the scope (§4.4, outside_scope); a room receipt or a statement that states a tariff states this one (§3, tariff_conflict); and it is in the invoice's period (§5.3, outside_period).
  6. Across sources, at source and the index of the later: a head contributed twice (record_duplicate); a receipt, statement or ad receipt overlapping an earlier one (§5.3, record_overlap); and a record that is in the previous invoice's records (record_rebilled).
  7. The figures, at invoice, recomputed from the sources as §6.2 to §6.6 compute them and compared in this order:
    1. a line whose source and unit no source gives, or that states a larger quantity than the sources give: quantity_not_recorded, a quantity no signed record covers;
    2. a row that §6.6 calls summed across classes: classes_summed;
    3. a line whose cited or cites is not the records it came from: citation_mismatch;
    4. any other difference (the records, a line missing, a smaller quantity, a price, an amount, a row, unmetered, the amount): invoice_mismatch.

A figure that cannot be held in its field is a difference.

7.3 Outcome

  • refuse: the reason and where.
  • accept: the invoice's digest; the tariff's digest; the currency and the period; the number of records; each line with its source kind, source, unit name (and, for a job line, the job price's capability), quantity, price, amount and its cited records; each energy row by scope and class label with its figures, joules and max_ppm; each scope's unmetered figures and coverage (scopes 1 to 4, and in version 2 scope 5); the amount; and the reports of §8.

8. Reports

The reports are computed from the verified sources, as the lines are, and are stated with every accepted invoice. They are not signed: anyone holding the sources computes them, and a verifier compares them.

8.1 Ad decisions against presentation claims

For each ads source, in order: its room, whether its policy requires claims, and each receipt in its list's order, with its identity, decider, cue and session, and each impression's slot, ad_id, price_micros and quartiles_reached, whether it is billable (the decider's beacons: a quartile of at least 1), whether it counts, and its verdict: under a policy that requires claims, presented or the reason its own claim did not count, claim_absent among them (ad-presentation §9.2); under one that does not, beacons. Then the receipt's billable impressions and their spend, budget_spent_micros, the seller's figure; and its counted impressions and their spend, the presented spend (ad-presentation §9.3), the buyer's. Then, over the source: the billable and the counted impressions, the billable and the counted spend, and, for each verdict other than presented, the billable impressions that did not count with it.

So a seller's receipts and a buyer's claims are reconciled from the same signed bytes: what the seller billed, what the players presented, and why each impression that was billed and did not count did not.

For each stitched source, in order, the report is the statement of stitched-presentation §7.5 as that extension's verifier gives it, unchanged: each record's verdict, each ad's presentations, quartiles and presented spend, and, for a checked form-1 response, the receipt's own quartile and spend beside them; each claim's verdict; and the spend by currency, never summed across currencies. That statement is the seller-against-buyer reconciliation of a stitched stream, and the ledger computes no second one.

8.2 Delivery by party

For the delivery sources together, each party in ascending order of key: the digests of its statements; their currency (the stated one, or null); the acknowledged objects and bytes of each class; the free retrieval bytes; the declared declarations, objects and bytes (settlement §4), never billed; the sum of its statements' own amounts; the sum of the invoice's lines of the party; and its energy rows and unmetered figures over its per-hop records.

Then the overlaps: for each pair of acknowledgements (mode 1) of two different parties, signed by one downstream key, whose windows overlap, and each content hash that an item of each names with byte ranges that share bytes, one overlap. It states the content hash, the downstream key, the bytes shared, summed over every such pair of items, and the two parties with their acknowledgements' digests, the lower party first. The overlaps are in ascending order of (content hash, downstream, first party, first record, second party, second record). An overlap is billed by both parties, since each was acknowledged; it is what a buyer using several delivering parties disputes with them, and the report names the signatures it rests on.

9. The reference ledger service

wai/wai-ledger runs the ledger as an HTTP service over a directory it owns:

request does
GET /v1 the service, its seller key, its limits
GET /v1/health ok
POST /v1/tariffs {"tariff": hex}: a tariff whose seller is the service's key, both signatures verified (§4), kept by its digest
POST /v1/sources {"tariff": digest, "source": {...}}: the source verified (§7.2 steps 5 and 6 against what the tariff's chain holds), kept by its heads
POST /v1/invoices {"tariff": digest, "start", "end"}: the invoice of the period over the sources held and not yet billed, chained, signed and kept, with its outcome (§7.3)
GET /v1/invoices/{digest} the invoice, its outcome and its verification document (§7.1)
POST /v1/verify a verification document: its outcome

Each refusal names its reason and place. The service writes every file whole (to a temporary name, synced, then renamed), holds a lock on its directory so that one process writes it, and recovers each tariff's chain and held sources from the directory when it starts. A buyer fetches an invoice's verification document and checks it with any verifier of this extension, the service's own not among them.

10. Reasons

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

reason raised at
tariff_invalid §4.3, §7.2 step 1
invoice_invalid §6.8, §7.2 step 2
tariff_mismatch §7.2 steps 3 and 4
outside_tariff §7.2 step 3
invoice_overlap §6.9, §7.2 step 4
source_gap §5.1, §7.2 step 5
outside_scope §4.4, §7.2 step 5
tariff_conflict §3, §7.2 step 5
outside_period §5.3, §7.2 step 5
record_duplicate §5.2, §7.2 step 6
record_overlap §5.3, §7.2 step 6
record_rebilled §6.9, §7.2 step 6
quantity_not_recorded §7.2 step 7
citation_mismatch §6.5, §7.2 step 7
invoice_mismatch §7.2 step 7
private_kind §5.2, §6.8, §7.2 step 2
output_invalid §5.1, §7.2 step 5
job_figure_invalid §5.1, §7.2 step 5

A verifier of this extension also raises truncated, malformed and unknown_version for a tariff or an invoice that does not read, and malformed for a source that does not read (§5.1); bad_key and bad_signature for their signatures; record_missing (live-metering §9) for a previous invoice not given; classes_summed (live-metering §9) for a row over classes; and, at a source, every code its extension's verifier raises: live-metering §9 and the codes it lists, settlement §11 and the codes it lists, packaging §12, live-ads §10 with ad-presentation §11, and, at a job source, route-record §8 and the codes it reuses. A stitched source raises none: the codes of stitched-presentation §9 are the verdicts of its records and claims, stated in its report (§8.1).

11. Conformance

live-conformance/ledger/ holds the corpus, in one file, vectors.json, written by wai_receipt_ledger_vectors gen from fixed seeds and from sources taken from the corpora of the extensions it reads (live-conformance/metering/, live-conformance/settlement/, live-conformance/ads/, live-conformance/presentation/, live-conformance/stitched/, live-conformance/routes/ and pack-conformance/packs/), with delivery statements of two parties signed for it and, for each media job, a sink's wai.video.int_motion decode receipt of its output over a payload of wai-rs/tests/vectors/int_video/. The sources are held once, by name, and each vector is a verification document (§7.1) naming its sources, with the outcome §7.3 gives it:

  • Accepted: an invoice of one day billing a lesson room, two delivering parties and a viewer that forwarded sealed objects under no tariff of its own, a package, ads under a policy that requires claims and under one that does not, with both reports, one viewer acknowledging one object from both parties among the overlaps; the next invoice of its chain, billing a relay's session whose energy is unmetered, and a second package; and an invoice whose room hops are of three classes, one row each; and an invoice of streams stitched by others, one with a form-1 receipt whose own figures the report states beside the presented ones; and, under a tariff of version 2, a month of media jobs one router routed, beside a package: an int_motion job billed per job and per subpixel of its output's receipt, a failover billed at the fallback's price, a job served past its deadline and one every allowed hop failed, which bill nothing, and a job whose serving hop was unmetered, counted unmetered in the jobs' scope.
  • Refused: each reason of §10 at least once, each vector a control of one defect; a source of each kind outside the scope, a room receipt and a statement under other prices, and a source of each timed kind outside the period; a refusal of each source kind by its own extension; and, for version 2, job prices out of order, a router in a version-1 tariff's scope, a job source under a version-1 tariff, a version-1 invoice under a version-2 tariff, a job given twice, routed twice, and claiming another job's output, and a job line and a jobs row that do not recompute; and an invoice of each version listing a record of a kind reserved for private use, and one listing another text's kind.

wai_receipt_ledger_vectors verify and the test tests/receipt_ledger.rs check that the file regenerates byte for byte, that wai-rs gives every vector its outcome, that every reason of §10 refuses a vector, and that §10 lists exactly the reference's codes. live-conformance/receipt_ledger_verify.py is the second implementation, written from this text: it shares no code with wai-rs, verifies signatures with WAI's strict rule written from RFC 8032 (claim-conformance/_ed25519.py), verifies each source with that corpus's own second implementation (live_metering_verify.py, settlement_verify.py, live_ads_verify.py with ad_presentation_verify.py, stitched_presentation_verify.py, route_record_verify.py, and pack-conformance/pack_verify.py) and each output receipt by _int_motion_receipt.py, written from integer-payloads §15, recomputes every line, citation, row and report itself, gives every vector the same outcome, and requires every mutated copy of the corpus to fail.

12. Security and privacy considerations

  • Who signs what. The buyer signs the prices and the scope; each record's own party signs its figures; the seller signs the invoice. A seller cannot bill a unit at a price the buyer did not sign, a record of a party the buyer did not name, or a quantity no record states: the verifier recomputes every line.
  • Double billing. A head is billed once in an invoice (record_duplicate), one session, one statement period and one viewer's break once (record_overlap), and a record once in a chain (record_rebilled). A verifier given one invoice checks it against the one before it; a buyer holding the chain checks every link.
  • What the figures rest on. An invoice is as true as its records: a delivery line rests on acknowledgements, so a party and a downstream key that collude can bill bytes that never moved, as settlement §13 says; a room line rests on per-hop records and the room's events; an ad line under claims rests on players' session keys. The reports name the signatures each figure rests on, so a dispute is about records, not about logs.
  • Energy. A row states figures as their parties signed them, with their class: it is attributable and its sum recomputable, not true (energy-measurement §5). max_ppm bounds the declared part of a sum's uncertainty and says nothing of a class's undeclared systematic error.
  • What an invoice reveals. Its records list names every record by identity, and its lines name rooms, parties, packagers and deciders. A verification document holds the records themselves: acknowledgements name downstream keys, and ad receipts name sessions. They travel between the seller, the buyer and an auditor they choose; the service serves an invoice's document by its digest and lists nothing.