πŸ₯€BevFacts

Technical / Part 6

Part 6 β€” QR & Cup Identity

The machine-readable half of the label: QR payloads, the compact URI scheme, cup IDs with check digits, the return lifecycle, and deposit accounting.

DBNF-1 Β· Part 6 Normative Status: Draft (label strip live)

6.1QR payloads

Every L3 label β€” and every Part 1 Β§1.5 compact label β€” carries a QR. Three payload forms, in order of preference:

FormPayloadWhen
Short linkhttps://<host>/l/{label_id}An L3 backend exists. Smallest module count β†’ most scannable on curved, wet cups.
Deep linkPart 5 Β§5.1 fragment URLNo backend β€” the label is self-contained; the QR is the database.
Compact URIdbnf1: scheme (Β§6.2)Closed-loop scanners (return kiosks, dish lines) that never open a browser.

6.1.1 Physical QR requirements

ParameterRequirement
Error correctionLevel M minimum; Q SHOULD be used on cold cups (condensation abrasion)
Module sizeβ‰₯ 0.33 mm printed (203 dpi: β‰₯ 3 dots/module)
Quiet zone4 modules all sides; MUST NOT wrap > 15Β° of cup curvature (Part 1 Β§1.6.3)
Payload budget≀ 2,048 bytes (fits QR version 25 @ EC M with margin)
ContrastDark-on-light only; inverted QRs fail too many scanner defaults

6.2The dbnf1: URI scheme

dbnf-uri   = "dbnf1:" payload
payload    = "l/" label-id            ; a Part-5 label reference
           / "c/" cup-id              ; a bare cup identity (return scans)
           / "s/" b64-state           ; an inline Part-5 Β§5.1.1 state blob
label-id   = 1*32( ALPHA / DIGIT / "_" / "-" )
cup-id     = <Β§6.3 grammar>
b64-state  = 1*2048( ALPHA / DIGIT / "+" / "/" / "=" )

Examples: dbnf1:c/A92F-3 Β· dbnf1:l/lbl_9f2ke1. Return-line scanners MUST accept all three forms and MUST extract a cup ID from form l/ by resolving the label (or rejecting gracefully offline). Browsers won't resolve dbnf1: β€” it exists precisely for hardware that shouldn't need the web to accept a cup back.

6.3Cup ID grammar & check digit

cup-id    = body [ "-" check ]
body      = 4*8HEXDIG            ; uppercase
check     = HEXDIG               ; Luhn mod-16 over body

Fleets over 50,000 cups SHOULD use the check digit β€” hand-keyed returns at busy counters mistype, and a wrong-cup refund is a ledger correction nobody enjoys. The algorithm is Luhn generalized to base 16:

function luhn16(body) {                 // body: uppercase hex string
  let sum = 0, dbl = true;              // double from the rightmost digit
  for (let i = body.length - 1; i >= 0; i--) {
    let d = parseInt(body[i], 16);
    if (dbl) { d *= 2; if (d >= 16) d = d - 16 + 1; }
    sum += d; dbl = !dbl;
  }
  return ((16 - (sum % 16)) % 16).toString(16).toUpperCase();
}
luhn16("A92F")   // β†’ "3"  β‡’ full ID "A92F-3"
// verify: luhn16(body) === check ─ single-digit errors and adjacent
// transpositions are detected

The label strip renders the ID as CUP #A92F-3 in monospace (Part 1 Β§1.2 row 16). IDs are unique per fleet, not globally β€” the tuple (fleet, cup-id) is the global key, carried in API paths by the key's location scope (Part 5 Β§5.4).

6.4Lifecycle state machine

            issue          sale            return-scan        wash-log
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”
  β”‚ issued β”‚ ──▢ β”‚ clean  β”‚ ──▢ β”‚ in_use β”‚ ─────▢ β”‚returned β”‚ ──▢  β”‚washingβ”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”¬β”€β”€β”€β”˜
                     β–²              β”‚ TTL expiry                       β”‚ pass QA
                     β”‚              β–Ό                                  β”‚ cycles++
                     β”‚          β”Œβ”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
                     └─────────── lost β”‚         β”‚ retired │◀─────────┴ fail QA /
                       found &   β””β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   max cycles reached
                       rewash
TransitionTriggerSide effects
clean β†’ in_usesale scandeposit charged; label printed; label.created
in_use β†’ returnedreturn scan (Β§5.5)refund instruction issued; cup.returned
in_use β†’ lostno return within fleet TTL (default 30 days)deposit forfeits to the reuse fund; cup.lost
lost β†’ washinglate return ("found")no automatic refund; fleet policy decides
washing β†’ cleanwash log (batch, temp β‰₯ 71 Β°C / 160 Β°F recorded)cycles++
washing β†’ retiredQA fail, or cycles β‰₯ max_cycles (vendor-rated)removed from fleet counts

All other transitions are invalid and MUST be rejected with 409 invalid_transition β€” including double returns, the classic deposit-fraud vector.

6.5Deposit ledger

Deposits are liabilities, not revenue. A conforming ledger is double-entry per cup:

sale scan      DR customer_payment   1.00     CR deposit_liability   1.00
return scan    DR deposit_liability  1.00     CR refund_payable      1.00
TTL forfeit    DR deposit_liability  1.00     CR reuse_fund          1.00

Requirements: refunds MUST be honored across all locations sharing a fleet; the ledger MUST reconcile to count(in_use) Γ— deposit at all times; forfeited deposits SHOULD fund cup replacement rather than margin (this is what makes the program defensible to regulators and customers alike).

6.6Privacy considerations

  1. QR payloads MUST NOT contain PII β€” no customer name, phone, or order ID that joins to one. A cup identifies a vessel, not a person.
  2. Return scans MUST NOT require an account; anonymous cash-out is the baseline. Loyalty linkage is opt-in at scan time.
  3. Label documents are recipe data and MAY be public (that's the point); scan event streams are behavioral data and MUST NOT be published at individual-cup granularity β€” publish aggregates (return rate, cycle counts).
  4. Fleet TTL processing means retaining scan timestamps; retain the minimum, and never join them to payment instruments beyond the refund transaction.