WAI Extension: Shoppable Events
Mirrored from the canonical text at commit c5e7133e ().
Status: Draft. Registers the record
wai.live.offer: an offer a room presents (an item, a price, a window of time) that a viewer can act on, with every step on a signed record, so that who is owed what recomputes. An offer is the payloadWOF1of a signedmetaevent of live-events, by a room key or a principal the room’s authority admits, anchored to the media object the programme shows it at (§5). It names the merchant’s item by the merchant’s own identifier and by the digest of the item’s signed description (§4), states its price as an integer of minor units, its window in the room clock, the regions it may be shown in, and its stock as a count or as unknown, never as zero when unknown (offer_stock_zero_when_unknown). A sink presents an offer from what the room carried and sends nothing until the viewer acts (§6). A viewer who acts signs an intent under a key made for the session (§7); the merchant’s endpoint answers it with a signed order or a signed refusal (§8), and a refund is a signed reversal (§9). The purchase is the merchant’s: this extension processes no payment. A merchant’s signed tariff states the share of an order the room’s operator, the creator and the delivery path are each owed, and a ledger’s signed statement lists what each is owed line by line, each line naming its order, its intent and its offer; a verifier given the records recomputes every line (§10). An order with no verifiable intent earns nothing, and a reversal takes back exactly what its order earned. No new transport and no new cryptography. The reference implementation is theshoppable_eventsmodule ofwai-rs(featureshoppable_events); the corpus islive-conformance/offers/, and its second implementation islive-conformance/shoppable_events_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.Texts read for this revision: WAI’s own, and the mandates of this repository’s payments standard as its
op-agenticcrate reads them (openpay/crates/op-agentic/src/mandate.rs), with its metered holds (openpay/crates/op-metered). RFC 8032, RFC 8259, RFC 8785, RFC 4648 and FIPS 180-4, as the texts this one builds on cite them. ISO 4217 and ISO 3166 were not read: a currency is bound here only as three capitals with a stated exponent, and a region only by the structure of its code (rung-price §2). No consumer-protection statute or regulation was read as its text for this revision; §13 states what the extension ensures and what it does not.
1. Scope
In scope: the item description, its digest and signature (§4); the offer, its carriage in a
meta event, its members, its stock, its region rule, who may offer, and how it is read (§5);
what a sink presents, the text of a price, the region aggregated in the sink, what leaves a
sink and when, and the key an intent is signed with (§6); the intent (§7); the hand-off, the
order, the refusal and the payment an intent may name (§8); the reversal (§9); the tariff, what
an order earns, the statement and its verification (§10).
Out of scope: a transport; payment, its authorisation, capture and refund, which are the merchant’s and its payment service’s; delivery of goods, tax, returns and every other term of a sale, which the merchant’s own terms state; how a merchant keeps stock; how a sink learns where it is; how a sink lays an offer out on a screen beyond what §6.1 requires it to show; the room, its grants and its event policy, which live-rooms and live-events define and this extension reads and does not change; and the invoice of receipt-ledger, which prices delivery, rooms and ads and to which this extension adds no source.
Relationship to the core spec: wai.live.offer is a record (SPEC §5): signed
records a WAI tool emits and verifies. It names no payload format and is not dispatchable.
2. Terms
- Document, canonical form, digest, token, key, signature and time: as route-record §2. A time is in microseconds since 1970-01-01T00:00:00Z.
- Room, room clock, principal, event, anchor: as live-events §2 and §3.1. A room key is live-rooms §2’s.
- Region code, its country, and holds: rung-price §2.
- Merchant: an Ed25519 key that signs item descriptions, orders, refusals, reversals and tariffs: the seller of record of what an offer offers.
- Item: what a merchant sells, named by a token of the merchant’s own choosing.
- Minor unit: the smallest unit a price is stated in. An amount is an integer of minor
units; an offer states with it a currency (three capitals
A–Z) and an exponent (0 to 6): the number of decimal digits a minor unit is of the currency’s unit. - Viewer key: the Ed25519 key a sink makes to sign intents (§6.5).
- Position: one object of one MoQ track, written as an anchor is:
track(64 lowercase hexadecimal digits, live-events §3.1),groupandobject(integers). - Ledger: an Ed25519 key that signs statements (§10.3).
- ppm: parts per million, an integer from 0 to 1 000 000.
3. Why these records
- An offer is part of the programme. It is an event of the room, signed as every event is, chained in its author’s chain and anchored to the object the programme shows it at (live-events §3.1). A relay can drop it and cannot alter it, and a recording holds it where it was shown.
- A price is what was signed. The price a viewer sees, the price the intent accepts and the price the order charges are one integer of minor units in one currency, and each record is held to the one before it.
- Unknown is not zero. A stock of zero says an item is sold out; a stock that is not known says nothing. An offer writes the two apart (§5.3), and one that writes unknown as zero is refused.
- A viewer who does not act is not seen. Everything a sink needs to present an offer arrives in the room. Nothing about an offer leaves a sink until its viewer acts, and then one request does, to the merchant, under a key made for the session (§6.4, §6.5).
- The sale is the merchant’s. The standard carries the viewer’s intent to the merchant and the merchant’s signed answer back. Payment is taken, or not, by the merchant.
- What is owed follows from the records. A share is a tariff’s rate times an order’s total, rounded down once, and every line of a statement names the order, the intent and the offer it came from. An order that cites no intent a verifier can verify earns nothing.
4. The item description
4.1 Members
A description is a document of exactly these members:
| member | form |
|---|---|
kind | "wai-offer-item" |
version | 1 |
merchant | a key: the merchant |
id | a token of at most 64 bytes: the merchant’s own identifier of the item |
title | a string of 1 to 120 bytes of UTF-8 holding no code point below U+0020 and not U+007F: what the item is |
seller | a string of the same form, of 1 to 64 bytes: the name the merchant sells under |
endpoint | a string of 9 to 256 bytes, each from ! to ~ (0x21–0x7E), beginning https:// or http:// and holding no ?, #, @ or \: where an intent for the item is handed off (§8.6) |
sig | a signature (§4.2) |
4.2 Digest and signature
The digest of a description is SHA-256 over the canonical form of the description without
sig, written as 64 lowercase hexadecimal digits. The signature is Ed25519 by merchant
over wai:offer-item (ASCII) and 0x01, followed by that same canonical form, verified strictly
as SPEC §7 Signatures verifies every signature. A signature over any other
bytes fails.
Every document of this extension is digested and signed the same way, each under its own
domain: the canonical form without sig, after the domain’s bytes and 0x01.
4.3 Reading
A reader is given a text. It refuses it offer_item_malformed when the text holds more than
4 096 bytes, is not a document, or its members are not exactly §4.1’s, each of its form; and
then offer_item_signature_invalid for its signature.
A description is carried in the room: as a meta event (live-events §4.2) whose payload is
the four bytes WOI1 followed by the description’s canonical form, by any principal its policy
permits meta. What makes a description the merchant’s is its signature, whoever carries it.
A sink holds the description of an offer when it has read one, from the room, whose digest
is the offer’s description.
5. The offer
5.1 Carriage
An offer is the payload of a meta event (live-events §4.2): the four bytes WOF1 (ASCII)
followed by the canonical form of the offer document (§5.2), in UTF-8, and nothing else. The
offer has no signature of its own: the event’s is its signature (live-events §3.2), so the
price, the window and every other member are signed with the event, by its principal, in that
principal’s chain.
The event MUST carry an anchor (live-events §3.1): the object of the programme the offer appears at. A sink presents the offer from the instant it presents that object (§6.1).
The id of an offer is the digest of its event (live-events §3.2), written as 64 lowercase hexadecimal digits. An intent, an order and a statement’s line name an offer by its id.
5.2 Members
| member | form |
|---|---|
kind | "wai-offer" |
version | 1 |
merchant | a key: the merchant whose item it is |
item | a token of at most 64 bytes: the item’s id (§4.1) |
description | a digest: the item’s description’s (§4.2) |
amount | an integer from 0 to 1 000 000 000: the price of one, in minor units |
currency | three capitals A–Z |
exponent | an integer from 0 to 6 |
start | an integer: the room clock from which the offer is open |
end | an integer above start: the room clock from which it is closed |
show | "everywhere", "only" or "except" (§5.4) |
regions | 0 to 32 region codes, strictly ascending by their bytes, with no subdivision code whose country it also lists; none exactly when show is "everywhere" |
stock | an object of stated and count (§5.3) |
An offer is open at room clock t when start ≤ t < end.
5.3 Stock
| member | form |
|---|---|
stated | true or false |
count | an integer when stated is true: how many the merchant stated it had when the offer was signed; null when stated is false |
A stock that is not known is written {"count": null, "stated": false} and in no other way.
An offer whose stated is false and whose count is 0 is refused
offer_stock_zero_when_unknown (§5.6): one reader would take it as unknown and another as
sold out. A stated count is the merchant’s statement at one instant, and whether an item can
still be had is the merchant’s answer to an intent (§8).
5.4 The region rule
show and regions state where the offer may be presented, as a territory statement’s do
(audience-decision §5.1): everywhere, only in regions, or everywhere
but in regions. A sink holds its location to the rule after aggregating it (§6.3), and the
rule is applied to that aggregation, written as audience-decision §4.3 writes evidence, an
object of region (a region code or null) and resolved:
"everywhere": the offer is eligible whatever the evidence;"only": eligible when the evidence has aregion(a listed code, whichregionshold); not when it is outside or unresolved;"except": eligible when the evidence is outside (resolved, noregion); not when it has aregionor is unresolved.
An unresolved location is never eligible under "only" or "except": a sink that cannot
decide the rule does not present the offer.
5.5 Who may offer
An offer’s event is its principal’s. A verifier is given the admitted keys of the room at
the event, and refuses an offer whose principal is not among them
(offer_author_not_admitted). They are:
- the room keys (live-rooms §2): the authority’s and the clock’s;
- each principal whose grant in effect at the event’s
t(live-rooms §9.2) has a policy that permitsmetaand carries the attributeofferwith the valuepublish(live-rooms §4.4).
The attribute is the authority’s signed statement that the principal may present offers in the
room. A grant admits when its policy permits meta and its attrs hold that entry. Which
grant is in effect, and that it is the authority’s and good at t, are live-rooms §9’s to
decide; this extension reads only the two facts above from a grant that section accepted.
5.6 Reading an offer
A reader is given the room id, an event’s record, the admitted keys, and the item’s description as a text or none. In this order; the first check that fails names the refusal:
- The record reads (live-events §3.3): otherwise
offer_event_malformed. - Its
kindismeta: otherwiseoffer_not_meta. - Its
roomis the room id: otherwiseoffer_wrong_room. - Its signature verifies (live-events §3.2): otherwise
offer_signature_invalid. An offer whose price, window or any other byte was changed after it was signed fails here. - Its payload begins
WOF1: otherwiseoffer_no_magic. - The rest of the payload is a document: otherwise
offer_malformed. Itsstock, when it is an object whosestatedisfalseand whosecountis0:offer_stock_zero_when_unknown. Its members are exactly §5.2’s and §5.3’s, each of its form, and the rest of the payload is the document’s canonical form, byte for byte: otherwiseoffer_malformed. - The event has an anchor: otherwise
offer_not_anchored. - Its principal is an admitted key (§5.5): otherwise
offer_author_not_admitted. - With a description: it reads (§4.3), with its own reason otherwise; and its digest is the
offer’s
description, itsmerchantthe offer’s and itsidthe offer’sitem: otherwiseoffer_item_mismatch.
A room’s verifier (live-events §6) carries the event as any meta event and holds it to its
principal’s policy, its chain and its rate. An offer’s payload is within the policy’s
max_payload as any payload is; §5.2’s bounds keep an offer within the default.
6. The sink
6.1 What a sink presents
A sink MAY present an offer, and one that does MUST present it only while all of these
hold: the offer reads (§5.6); the sink holds its description (§4.3) and the description reads
and matches (§5.6 step 9), otherwise offer_item_not_held; the sink has presented the object
the event’s anchor names, or one after it on that track; the offer is open at the room clock
of what the sink is presenting, otherwise offer_outside_window; and the offer is eligible
for the sink’s aggregated location (§5.4, §6.3), otherwise offer_region_excluded.
A sink that presents an offer MUST show, together and legibly, for as long as it invites the viewer to act:
- that it is an offer: a label that says so in words, apart from the programme’s picture;
- the price, as §6.2 writes it, for one item;
- the seller: the description’s
seller; - the item: the description’s
title; - when it closes, when the sink shows a time at all: the offer’s
end.
Of stock, a sink shows the stated count, or that none is left when the stated count is 0, in which case it MUST NOT invite the viewer to act. Where stock is not stated it shows nothing of stock: it MUST NOT show an item as sold out, as available, or as scarce.
A sink MUST NOT act for the viewer: an intent is signed only on the viewer’s own action on the presented offer, after the price was shown.
6.2 The text of a price
The price text of an amount a, an exponent e and a currency c is: the decimal digits
of ⌊a / 10^e⌋ with no leading zero (0 for zero); when e is above 0, a full stop
(U+002E) and the e decimal digits of a mod 10^e, with leading zeros; a space (U+0020);
and c. An amount of 1250 with exponent 2 and currency XTS is 12.50 XTS. A sink shows the
price text, and MAY show beside it the same amount in a form its locale writes.
6.3 The region, aggregated in the sink
A sink’s location never leaves the sink. It is aggregated against the offer’s regions
exactly as audience-decision §4.3 aggregates a session’s location against listed codes: to the
one listed code that holds it, to outside, or to unresolved. The evidence an intent carries is
that aggregation and nothing else. How a sink comes to hold a location is out of scope.
6.4 What leaves a sink, and when
Until the viewer acts on an offer, nothing about it leaves the sink. A sink MUST NOT:
- send a request to the merchant, to the room’s service or to any other party because an offer arrived, was presented, was seen or was passed over. The description is carried in the room (§4.3), so presenting needs none;
- make, keep for sending, or send a record that an offer was presented to a viewer who did not act on it, or count such presentations in what it reports. No observation of delivery-telemetry carries an offer, an item or a merchant;
- fetch anything from the description’s
endpoint, or resolve its host, before the viewer acts.
When the viewer acts, the sink signs one intent (§7) and hands it off (§8). That request carries the intent and the offer’s event and nothing else of the viewer: no cookie, no stored identifier, no referrer, no address beyond what carrying a request needs. The intent names the viewer by the viewer key alone.
6.5 The viewer key
A sink MUST sign intents with an Ed25519 key it makes from its platform’s random generator
for one playback session of one room at most, and MAY make one for each intent. It MUST
NOT use the key in another room or another session, derive it from an identifier of the
viewer, the device or an account, or use the key of its room grant for it. The key is fresh
for a session as the session identifier sid of delivery-telemetry §8 is, and says no more of
who the viewer is.
Who the buyer is, where the sale needs it, is between the viewer and the merchant, after the
hand-off and outside this extension.
7. The intent
7.1 Members
An intent is a document of exactly these members:
| member | form |
|---|---|
kind | "wai-offer-intent" |
version | 1 |
viewer | a key: the viewer key (§6.5) |
room | 64 lowercase hexadecimal digits: the room id |
offer | a digest: the offer’s id (§5.1) |
merchant, item, amount, currency, exponent | the offer’s, as the viewer was shown them |
quantity | an integer from 1 to 1 000 |
position | a position (§2): the object the sink was presenting when the viewer acted |
t | an integer: the room clock of that object |
evidence | an object of region and resolved (§6.3); resolved is false only with a region of null |
payment | null, or an object of exactly mandate, a digest (§8.5) |
nonce | 32 lowercase hexadecimal digits the sink draws at random |
sig | a signature |
The digest and the signature are §4.2’s, by viewer, under the domain wai:offer-intent.
7.2 Reading and rules
A reader is given an intent’s text and the offer it is for, read as §5.6 reads it. In this order:
- The text holds at most 65 536 bytes, is a document, and its members are exactly §7.1’s,
each of its form, where
evidenceis an object that holdsregionandresolved, each of its form: otherwiseintent_malformed. evidenceholds no member butregionandresolved: otherwiseintent_location_not_aggregated.- Its signature verifies: otherwise
intent_signature_invalid. - Its
roomis the room of the offer’s event and itsofferis the offer’s id: otherwiseintent_offer_mismatch. An intent signed for another offer fails here. - Its
merchant,item,amount,currencyandexponentare the offer’s: otherwiseintent_price_mismatch. - A
regionthat is notnullis one of the offer’sregions: otherwiseintent_location_not_aggregated, since it says more of where the viewer is than the aggregation gives. - The offer is open at
t(§5.2): otherwiseoffer_outside_window. - The offer is eligible for the evidence (§5.4): otherwise
offer_region_excluded. - Where the offer’s stock is stated,
quantityis at most itscount: otherwiseintent_exceeds_stock.
An intent that passes is verified against its offer. t and position are the sink’s
statement of where the viewer was in the programme, signed by the viewer key, and an order and
a statement’s line carry them from the intent unchanged.
8. The hand-off
8.1 The request
The sink gives the merchant a hand-off: a document of exactly kind
("wai-offer-handoff"), version (1), intent (the intent, as an object) and offer (the
offer’s event record in base64url without padding, RFC 4648 §5). A merchant that cannot read
it refuses it handoff_malformed; one that holds no offer of its own with that record’s
digest, handoff_offer_unknown. Neither is signed: there is no intent to name.
A merchant reads the offer from its own copy, since it knows the room and who was admitted,
and reads the intent against it (§7.2). It answers a verified intent with an order (§8.2) or
with a refusal (§8.3), and an intent that does not verify with a refusal whose reason is
§7.2’s.
8.2 The order
An order is a document of exactly these members:
| member | form |
|---|---|
kind | "wai-offer-order" |
version | 1 |
merchant | a key: the merchant that accepts |
room, offer, viewer, position, currency, payment | the intent’s |
intent | a digest: the intent’s |
total | an integer: the intent’s amount times its quantity, in minor units |
issued | a time: when the merchant accepted |
sig | a signature, by merchant, under the domain wai:offer-order |
An order states that the merchant accepted the intent at that total. It does not state that payment was taken or that anything was delivered.
8.3 The refusal
A refusal is a document of exactly kind ("wai-offer-refusal"), version (1),
merchant (a key), intent (a digest), reason (a token: a reason of §11 the intent was
refused for, or a token of the merchant’s own, such as merchant_declined), issued (a time)
and sig, by merchant, under the domain wai:offer-refusal. A reader refuses a text that
is not one refusal_malformed, one whose signature fails refusal_signature_invalid, and one
whose intent is not the digest of the intent it was given for, or whose merchant is not
that intent’s, refusal_intent_mismatch. A refusal earns nothing and is no claim on anyone.
8.4 Reading an order
A reader is given an order’s text, the offer (§5.6) and intents as texts. In this order:
- The text holds at most 65 536 bytes, is a document, and its members are exactly §8.2’s,
each of its form: otherwise
order_malformed. - Its signature verifies: otherwise
order_signature_invalid. - One of the given intents reads, has the digest the order’s
intentnames, and is verified against the offer (§7.2): otherwiseorder_without_intent. - Its
vieweris that intent’s: otherwiseorder_viewer_mismatch. An order that cites the intent of one viewer key and names another fails here. - Its
merchant,room,offer,position,currencyandpaymentare that intent’s, and itstotalis that intent’samounttimes itsquantity: otherwiseorder_intent_mismatch.
An order that passes is verified.
8.5 Payment
This extension takes no payment and defines no payment form. Where the viewer pays under this
repository’s payments standard, the intent’s payment names the mandate the viewer’s
payment agent gave: mandate is SHA-256 over the bytes of the mandate’s presentation, as that
standard’s op-agentic reads a mandate (a verifiable credential in SD-JWT form, closed over
one checkout or open over a set of them). The order carries the same payment, so the
merchant’s acceptance is bound to the mandate it was offered. Whether the mandate verifies,
what its constraints allow, a hold placed for the total and captured at or under it (that
standard’s metered holds), and the payment itself are that standard’s and the merchant’s, and
no check of this extension reads a mandate. A payment of null says payment is arranged
between the viewer and the merchant by other means.
8.6 Over HTTP (mapping)
Field semantics only. A sink hands off by one POST to the description’s endpoint, with
content-type: application/json and the hand-off’s canonical form as the body. The merchant
answers 200 with an order or a refusal in its canonical form, or 400 for a hand-off it
does not sign an answer to (§8.1). The sink sends no credentials, no cookie and no referrer
with it (§6.4). It MUST NOT hand off to an http:// endpoint whose host is not a loopback
address. It reads the answer as §8.4 or §8.3 reads it before it shows the viewer an outcome,
and shows an order only when it is verified. What follows (payment, an account, delivery) is
between the viewer and the merchant.
9. The reversal
A reversal is a document of exactly kind ("wai-offer-reversal"), version (1),
merchant (a key), order (a digest), total (an integer), currency (three capitals),
issued (a time) and sig, by merchant, under the domain wai:offer-reversal. It states
that the merchant took the whole order back: a refund, a cancellation, a return.
A reader given a reversal’s text and an order’s refuses the reversal reversal_malformed when
it is not one (at most 65 536 bytes); reversal_signature_invalid for its signature; and
reversal_order_mismatch when its order is not the order’s digest, or its merchant,
total or currency is not the order’s. A reversal reverses a whole order. Part of an order
taken back is not written in this revision: a merchant that refunds part reverses the order
and its own terms state the rest.
10. The tariff and the statement
10.1 The tariff
A tariff is a document of exactly these members:
| member | form |
|---|---|
kind | "wai-offer-tariff" |
version | 1 |
merchant | a key: the merchant that owes the shares |
room | 64 lowercase hexadecimal digits: the room whose offers it covers |
currency | three capitals |
start, end | times, start below end: it covers the orders issued from start and before end |
shares | an object of exactly operator, creator and delivery, each an object of payee (a key) and ppm (0 to 1 000 000); the three ppm sum to at most 1 000 000 |
sig | a signature, by merchant, under the domain wai:offer-tariff |
The three roles are the room’s operator, the creator whose programme presented the offer,
and the delivery path that carried it. The tariff names one payee for each, by key; how the
parties agree its rates, and who the delivery payee pays in turn, are their own contracts’. A
reader refuses a text that is not a tariff (at most 65 536 bytes) tariff_malformed, and then
tariff_signature_invalid.
10.2 What an order earns
A ledger is given a room id, the admitted keys, a tariff, and records: offers’ events, and the texts of intents, orders and reversals. A text or a record that does not read is passed over. An order earns under the tariff when all of these hold:
- it reads and its signature verifies (§8.4 steps 1 and 2);
- its
roomis the room id and the tariff’sroom, itsmerchantthe tariff’s, itscurrencythe tariff’s, and the tariff’sstart≤ itsissued< the tariff’send; - its
offeris the id of one of the given events, which reads as an offer (§5.6, with no description), and the order is verified against it with the given intents (§8.4); - no other order of the given texts that meets 1 to 3 cites its
intentand has a digest below its own, as bytes: one intent earns once.
An order with no verifiable intent does not meet 3 and earns nothing. For each role whose
ppm is above 0, an order that earns gives one credit line of
⌊total × ppm / 1 000 000⌋
minor units to the role’s payee, computed in integers wide enough not to overflow. Each line is rounded down once; what the rounding leaves stays with the merchant and is added to no other line.
A reversal applies to an order that earns when it reads against it (§9). Of the reversals that apply to one order, the one of the least digest is counted, and gives one debit line for each of the order’s credit lines, of the same amount to the same payee. So a reversed order’s lines sum to nothing, and a reversal of an order that earned nothing gives no line.
10.3 The statement
A statement is a document of exactly these members:
| member | form |
|---|---|
kind | "wai-offer-statement" |
version | 1 |
ledger | a key: who computed it |
merchant, room, currency | the tariff’s |
tariff | a digest: the tariff’s |
lines | 0 to 3 000 lines |
owed | an object of exactly operator, creator and delivery, each an integer: the role’s credits less its debits |
sig | a signature, by ledger, under the domain wai:offer-statement |
A line is an object of exactly order, intent and offer (digests: the order, and the
intent and the offer it cites), reversal (null for a credit; the reversal’s digest for a
debit), role ("operator", "creator" or "delivery"), payee (a key), entry
("credit" or "debit") and amount (an integer of minor units).
Lines are in order: by the bytes of order; within an order its credits and then its debits;
within each, operator, creator, delivery. So every line names the records it came from,
and a payee that holds them recomputes its own.
10.4 Verification
A verifier is given what a ledger is (§10.2), the tariff as a text, and a statement’s text. In this order:
- The statement holds at most 1 048 576 bytes, is a document, and its members and its lines’
are exactly §10.3’s, each of its form,
reversalbeingnullexactly whenentryis"credit": otherwisestatement_malformed. - The tariff reads (§10.1), with its own reason otherwise.
- The statement’s
tariffis the tariff’s digest, and itsmerchant,roomandcurrencyare the tariff’s, and itsroomthe room id: otherwisestatement_tariff_mismatch. - Its signature verifies: otherwise
statement_signature_invalid. - For each line, in order: its
orderis the digest of a given order that reads and whose signature verifies, otherwiseattribution_line_without_order; and that order earns (§10.2), otherwiseattribution_order_earns_nothing. - Its
linesandowedare what §10.2 gives, line for line: otherwisestatement_not_recomputed.
A statement that passes is recomputed. It is the recomputable figures a payment to a payee rests on; it moves no money and is no invoice of receipt-ledger’s.
11. Reasons
| reason | raised at |
|---|---|
offer_item_malformed | §4.3 |
offer_item_signature_invalid | §4.3 |
offer_event_malformed | §5.6 step 1 |
offer_not_meta | §5.6 step 2 |
offer_wrong_room | §5.6 step 3 |
offer_signature_invalid | §5.6 step 4 |
offer_no_magic | §5.6 step 5 |
offer_stock_zero_when_unknown | §5.3, §5.6 step 6 |
offer_malformed | §5.6 step 6 |
offer_not_anchored | §5.6 step 7 |
offer_author_not_admitted | §5.5, §5.6 step 8 |
offer_item_mismatch | §5.6 step 9 |
offer_item_not_held | §6.1 |
offer_outside_window | §6.1, §7.2 step 7 |
offer_region_excluded | §6.1, §7.2 step 8 |
intent_malformed | §7.2 step 1 |
intent_location_not_aggregated | §7.2 steps 2 and 6 |
intent_signature_invalid | §7.2 step 3 |
intent_offer_mismatch | §7.2 step 4 |
intent_price_mismatch | §7.2 step 5 |
intent_exceeds_stock | §7.2 step 9 |
handoff_malformed | §8.1 |
handoff_offer_unknown | §8.1 |
order_malformed | §8.4 step 1 |
order_signature_invalid | §8.4 step 2 |
order_without_intent | §8.4 step 3 |
order_viewer_mismatch | §8.4 step 4 |
order_intent_mismatch | §8.4 step 5 |
refusal_malformed | §8.3 |
refusal_signature_invalid | §8.3 |
refusal_intent_mismatch | §8.3 |
reversal_malformed | §9 |
reversal_signature_invalid | §9 |
reversal_order_mismatch | §9 |
tariff_malformed | §10.1 |
tariff_signature_invalid | §10.1 |
statement_malformed | §10.4 step 1 |
statement_tariff_mismatch | §10.4 step 3 |
statement_signature_invalid | §10.4 step 4 |
attribution_line_without_order | §10.4 step 5 |
attribution_order_earns_nothing | §10.4 step 5 |
statement_not_recomputed | §10.4 step 6 |
12. Conformance
live-conformance/offers/ holds the corpus, in one file,
vectors.json, written from fixed seeds by wai_shoppable_events_vectors gen. It has:
- items (§4): descriptions’ texts, read with their digest or refused with their reason;
- admissions (§5.5): room grants, each admitting or not;
- offers (§5.6): a room id, an event’s record, the admitted keys and a description or none, read with the offer’s id, its price text and its members, or refused;
- prices (§6.2): amounts, exponents and currencies with their price text;
- presentations (§6.1, §6.3): an offer, a room clock, a sink’s location and whether it holds the description, with the evidence it aggregates to and whether the offer is presented, or the reason it is not;
- intents (§7.2), orders (§8.4), refusals (§8.3) and reversals (§9): each with the records it is read against, verified or refused;
- handoffs (§8.1): a hand-off and the offers a merchant holds, with what it answers;
- tariffs (§10.1), ledgers (§10.2: records and a tariff, with the lines and the sums they give) and statements (§10.4), recomputed or refused.
Every reason of §11 has a case refused with it. Among them, each a control of one defect: an intent for an offer outside its window; an offer whose price was changed after it was signed; an intent for another offer; an order that cites an intent of another viewer key; a statement’s line with no order; an intent from a region the rule excludes; and an offer whose stock is written zero when it is unknown.
A conforming implementation gives every case the outcome the corpus states.
wai_shoppable_events_vectors verify and the test tests/shoppable_events.rs check that the
file regenerates byte for byte and that wai-rs gives every case its outcome.
live-conformance/shoppable_events_verify.py is the second implementation, written from this
text and sharing no code with wai-rs; it also mutates copies of the corpus and drops each of
its own checks in turn, and requires each to be caught.
The room service wai-room publishes an offer and its description on the clock’s events
track for a caller whose grant holds the direct power (POST /offer). The browser client’s
web/offers/ presents an offer as §6.1 requires, signs an intent with the platform’s Ed25519
and hands it off as §8.6 maps it; its run against a fixture merchant on a loopback address
requires that no request leaves the page before the viewer acts.
13. What this extension ensures, and what it does not
What a conforming set of records ensures, each by a check above: that an offer was made by a key the room’s authority admitted and has not changed since; that a sink that follows §6.1 showed the price, the seller, the item and that it was an offer before the viewer acted; that the price in the intent and the total in the order are the price of the offer; that the offer was open, and eligible for the region the sink aggregated, when the viewer acted; that an order answers one intent of one viewer key; and that every line of a statement follows from a verified order under the tariff.
What it does not ensure, and no record of it states:
- That a presentation meets the law of any place. What a seller must show a buyer before a sale, in what language and form, with what taxes, charges, delivery terms, rights of withdrawal, age limits or product-specific warnings, differs by jurisdiction and by product. §6.1 is a floor an implementation is tested against, an engineering requirement of this extension; it is not a statement that a sink that meets it meets any statute. Meeting those rules is the merchant’s and the operator’s, in each place they sell.
- That the price is the whole price.
amountis what the merchant signed for one item. Whether it includes tax, or what delivery costs, is not a member of an offer in this revision, and a merchant whose law requires a total inclusive of them states an inclusiveamountor does not offer there (§5.4). - That a region rule is a legal boundary.
showandregionsare the merchant’s statement of where it offers, applied to a location the sink aggregated itself. They are not proof of where a viewer is. - That an order is a contract, a payment or a delivery. An order is the merchant’s signed acceptance of an intent. Whether and when a contract forms, and under whose law, is not decided here.
- That a viewer consented to anything. This extension sets no consent rule and says nothing of how a merchant or an operator identifies a person. It bounds what a sink sends (§6.4), and what a merchant asks of a buyer after the hand-off is the merchant’s.
- That an offer is marked as advertising where a rule asks for that. The label of §6.1 says that it is an offer. Whether a programme that presents offers must be marked in some other way is the creator’s and the operator’s to meet.
14. Security and privacy considerations
- Every member of an offer is signed with its event. A relay can drop an offer, which is a gap the receiver detects (live-events §6.2), and cannot change a price or a window.
- An offer’s author is a key the authority admitted, by a grant’s signed attribute or as a
room key. A participant whose policy permits
metaand whose grant lacks the attribute can publish ametaevent that beginsWOF1, and every reader refuses it (offer_author_not_admitted); a sink never presents it. - The description is the merchant’s signed statement of what is sold and by whom. An offer by
an admitted principal for a merchant that never described the item has no description that
matches, and is not presented (
offer_item_not_held). - The window is stated in the room clock and an intent’s
tis the sink’s claim, as an event’stis its author’s (live-events §11). A merchant that needs more holdsissued, its own clock, to the window as it maps the room clock; this revision checkstalone. - A viewer key is made for a session and names nothing else (§6.5). A merchant that receives two intents under one key learns that one session made both, and no more; a sink that makes a key for each intent gives it not even that. The request that carries an intent reaches the merchant from the viewer’s network address, as any request does.
- A sink that presents offers sends nothing until its viewer acts (§6.4), so neither the merchant, the room’s service nor a relay learns which viewers were shown an offer, saw it or passed it over. A relay learns that a subscriber receives the events track, as for any track.
- The
noncemakes two intents of one viewer for one offer two records. A merchant that receives the same intent twice answers with the same order, and of two orders that cite one intent only one earns (§10.2). - A statement’s amounts are rounded down, each once, so no payee is credited more than its share and rounding never moves an amount between payees.
- The hand-off is one request to an endpoint the merchant signed. An
http://endpoint is for a loopback address alone (§8.6); elsewhere the endpoint ishttps://, and its confidentiality is that transport’s.