Lifecycle feature matrix
The canonical end-to-end map of a Kiosk-X vending machine's life — from the
pallet to decommission — with every cell backed by an executable eval.
Rows are lifecycle stages; manufacturer columns cover Reyeah (RK3288
board, com.ruiye.jd APK, /apk/* protocol) and ZHZN/CSM (T3568 board,
Python agent, /zhzn/* gateway); channel coverage spans the vending APK,
operator Android app, operator web console, the web emulator, and the
Partner API. A USA-operability column tracks currency, language, tax, and
compliance posture.
This is a living document: the eval-run ledger at the bottom records the
latest execution against the live stack (api.kiosk-x.ai).
How the evals run
| Layer | Command | What it proves |
|---|---|---|
| API evals (in-process) | pytest tests/test_lifecycle_matrix.py -q |
Every automatable cell against the app + in-memory store; runs in CI with the full suite |
| Live probes | python scripts/lifecycle_matrix_live.py --base https://api.kiosk-x.ai |
The same cells against the deployed stack, using a fresh throwaway operator + fresh machine serials (never mutates the shared demo fleet, never calls /sandbox/reset) |
| Emulator walkthrough | /emulator/lifecycle (10 steps, real routes) |
Browser-visible E2E: unbox → boot → claim → subscribe → stock → vend → refill → OTA → earning |
Cell IDs (LM-nn) appear in test names (test_lm05r_… = row 5, Reyeah
column) so a matrix cell can always be traced to its eval and back.
Lifecycle at a glance
Status legend
- PASS — eval executed and green (evidence: suite/ledger row)
- FIXED→PASS — failed on first run; fixed in this repo, re-run green (commit in ledger)
- ROUTED — owned by another in-flight agent; verified state documented, not double-fixed
- BLOCKED-HW — needs the physical machine (RK3288 motor, Nayax reader tap, ZHZN test unit)
- BLOCKED-BIZ — needs business credentials (live Stripe keys, real Nayax merchant, DOOH network contracts)
- BLOCKED-CFG — code path exists and is tested in-process, but the live deployment doesn't enable it yet (env/secret)
The matrix
Channels key: API · Emulator · Vending APK · Operator APK · Web console
| # | Stage | Reyeah | ZHZN | Channels verified | USA operability | Evals |
|---|---|---|---|---|---|---|
| 1 | Physical install & unboxing | BLOCKED-HW (runbook in machine-client.md; demo unit ...011 installed) |
BLOCKED-HW (no ZHZN test unit; wiring doc in zhzn repo WIRING_AND_DEPLOY.md) |
— | 110 V US power, standard footprint | manual runbook |
| 2 | First power-on + network | BLOCKED-HW live · PASS emulated (boot step drives real routes) | BLOCKED-HW live · PASS agent-emulated (fake-VMC PTY harness) | E | English boot UI pending OTA (ROUTED: RCA/OTA agent) | lm-walkthrough boot, zhzn agent E2E |
| 3 | Auto-provision on first contact | PASS — unknown serial on /apk/getEquipment lands in unclaimed pool, boots to working USD grid; strict mode rejects |
PASS — /zhzn/register maps serial→machine code, provisions; fleet-secret auth; strict-map allowlist |
A, E | USD grid from first boot | LM-03r, LM-03z |
| 4 | Operator signup / onboarding | PASS — admin creates account, operator signs in (Intelliverse or demo password), sees empty fleet; onboarding checklist | same (manufacturer-agnostic) | A, W, O | English console; US operator accounts | LM-04 ×2 |
| 5 | Machine registration / claiming | PASS — fresh-serial register AND claim-of-provisioned-board with unclaimed-vend transfer; cross-tenant 409 | PASS — ZHZN board claimable through the same Partner API | A, E, W | — | LM-05r ×2, LM-05z |
| 6 | Nayax terminal binding | PASS — real Device Number bind replaces provisional placeholder; webhook resolves bound terminal only; conflict 409; physical reader tap: BLOCKED-HW | n/a hardware path (ZHZN uses MDB-RS232; same cloud binding applies) | A, W | Nayax = US cashless standard; real merchant creds BLOCKED-BIZ | LM-06 |
| 7 | Planogram / product catalog | PASS — operator aisle edit ⇄ device getGoods; on-device manager-menu save persists |
PASS via Partner API (grid config served by /zhzn/config) |
A, V(manager menu), W | Catalog imagery reseed ROUTED: demo-imagery agent | LM-07 ×2 |
| 8 | Pricing in USD + tax | PASS — catalog, goods, and equipment config all $ (¥→$ landed in 31b4ad7, now a hard regression gate); salesTaxRate per machine + tax-inclusive liability estimate in orders summary |
PASS (same pricing store) | A, E, W | Core USA row: USD everywhere, tax-inclusive posture (operator remits from gross), rate validation | LM-08 ×4 |
| 9 | Restock flows | PASS — fill-all and per-aisle | PASS (same inventory API) | A, E, O, W | — | LM-09 |
| 10 | Vend: cash | PASS — settles immediately as cash processor, stock decrements |
PASS at gateway level (agent reports outcome; motor BLOCKED-HW) | A, E | USD | LM-10r |
| 11 | Vend: card / Nayax | PASS — order pending until signed webhook settles | same webhook path once terminal bound | A, E | Real Nayax merchant BLOCKED-BIZ | LM-11r |
| 12 | Vend: scan-to-pay | PASS as designed — scan-to-pay surfaces are the QR pages (/play, /advertise); no card-on-file product checkout exists (product decision, not a gap) |
same QR surfaces | A, V(QR) | Stripe checkout surfaces ROUTED: Stripe-audit agent | LM-12 |
| 13 | Vend: multi-unit cart | PASS — cart num bills & decrements correctly; over-stock refused atomically |
gateway supports quantity per dispense | A | — | LM-13r |
| 14 | Vend: age-gated | PASS — unverified restricted vend refused; ageVerify one-shot unlocks exactly one transaction |
gateway machines inherit same order gate | A, V | Tobacco 21 enforced server-side; ID-scanner hardware calibration BLOCKED-HW | LM-14 |
| 15 | Vend: $0 promo / KX credit | PASS — demo-task completion mints machine-bound KX- code → validDiscountCode → $0 promo order; funding-source enforcement ROUTED: funding-enforcement agent (its suite: test_funding_enforcement.py) |
credits are machine-bound, manufacturer-agnostic | A, V(QR) | — | LM-15 |
| 16 | Settlement + webhook idempotency + stockMismatch | PASS — replay is a no-op; empty/faulted lane flags stockMismatch+needsRefund; unsigned webhook 401 |
ZHZN settlement forwards to UserManagement complete endpoint (exactly-once, deduped) | A | HMAC-signed webhooks; real Nayax secret BLOCKED-BIZ | LM-16 ×3 |
| 17 | Dispense-failure reconciliation | PASS — ordersUpdate MOTOR_JAM → needsRefund → operator refund books payState 4 |
PASS — failed report with orderId routes to dispense/complete with success:false |
A, O, W | — | LM-17r, LM-17z |
| 18 | Faults | PASS — submitFault marks lane faulted (blocks vends), operator sees reason, clears via aisle PUT |
agent surfaces board return-codes; same aisle-fault store | A, V, W | — | LM-18 |
| 19 | Telemetry / MQTT presence + remote dispense | PASS — api_drop command queue round-trip; MQTT bridge live (mq.kiosk-x.ai:1883 open); physical motor BLOCKED-HW |
PASS — dispense queue leased exactly-once with crash-safe re-delivery | A, E | — | LM-19r, LM-19z, test_mqtt_bridge.py |
| 20 | OTA upgrade | PASS — publish rollout → device offer → apkUp → 100% adoption auto-complete; pause/resume/cancel; rollout to physical ...011 ROUTED: RCA/OTA agent |
PASS — /zhzn/upgrade serves signed manifest (version/url/sha256) |
A, E, V | English/USD APK variant is the payload (ROUTED) | LM-20r ×2, LM-20z |
| 21 | Ads / attract loop | PASS — loop never empty, direct campaigns take priority, house fills; player-safe: no remote video until RK3288 sign-off; proof-of-play recorded | same loop served per machine | A, E, V | Real DOOH demand BLOCKED-BIZ (Vistar/PX/Blindspot creds); mediation mocks live | LM-21 (+127-test mediation matrix) |
| 22 | QR monetization on-machine | PASS reachability per machine; deep flows owned by their suites: ads chatbot (ROUTED: ad-chat resilience agent), game worlds (ROUTED: gameworld-bridge agent), spin/scratch/draws (test_prize_mechanics.py), QuestX (test_free_product_play.py), multiplayer (test_multiplayer_game.py) |
same | A, V(QR) | — | LM-22 |
| 23 | Subscriptions / billing | PASS — subscribe machine (USD) then cancel; billing account/wallet surfaces | same | A, W | USD plans; card-on-file/Stripe checkout ROUTED: Stripe-audit agent | LM-23 ×2 |
| 24 | Reporting / exports | PASS — orders summary (USD + tax estimate), payments summary, billing CSV export | same | A, W | USD reports | LM-24 |
| 25 | Migration off vendor cloud | PASS (cloud half) — migrated board's first contact auto-provisions, claim keeps continuity, apkUp reports build; downloads page ships kioskx APKs only. Physical APK swap on deployed units BLOCKED-HW + runbook below |
ZHZN units never used the vendor cloud (agent is ours from day one) — n/a | A, V | — | LM-25 ×2 |
| 26 | Decommission / transfer | PASS — new: POST /machines/{no}/decommission returns board to unclaimed pool, frees the Nayax Device Number, keeps revenue on the old operator's books; transfer = decommission + claim |
same endpoint | A, W | — | LM-26 ×3 |
| 27 | This matrix stays published | PASS — served at /guides/lifecycle-matrix with mermaid + S3 mirror |
— | W | — | LM-27 |
USA-readiness bottom line
Reyeah: USA-operable for cash + Nayax cashless vending today at the cloud
level — USD on every money surface (catalog, goods, orders, reports,
subscriptions), English console/docs, Tobacco-21 age gate enforced
server-side, tax-inclusive pricing with per-machine tax-liability estimates.
Server-side currency is now guaranteed (31b4ad7 forces $ in the
equipment config regardless of fixture drift). Remaining gaps are
device-side: the Chinese/vape-banner APK cleanup and the OTA rollout to
the deployed demo unit are in flight (RCA/OTA agent), and
motor/ID-scanner/Nayax-tap verification needs the physical machine.
ZHZN: cloud gateway fully implemented and tested (register / config /
pending / report / heartbeat / upgrade, exactly-once settlement forwarding),
USD/English by construction since the device runs our agent, enabled on the
live deployment since 2026-08-12 (KIOSKX_ZHZN_DEVICE_SECRET CI-bootstrapped
into kiosk-x-secrets and wired in the deployment — kube-infra b534dd65;
live proof: lifecycle_flavors_live.py run 8657773248, existing-zhzn PASS),
but never validated against a physical ZHZN unit (BLOCKED-HW). The
one-APK universality question is settled per the zhzn repo's committed docs:
a single shared APK across manufacturers is not feasible (incompatible
boards and wire protocols) — ZHZN ships as our headless Python agent speaking
/zhzn/*, which is exactly what this matrix's ZHZN column evaluates.
Migration runbook (deployed vendor-cloud machine → Kiosk-X)
Status: cloud path proven by eval (LM-25); device steps require a site visit.
- Build: CI produces
reyeah-vending-kioskx.apkwithUrlConfigString.baseUrlpatched tohttps://api.kiosk-x.ai(seeapk-rollouts.md); hosted on the downloads page. - On the machine (manager PIN → Android settings): uninstall the vendor APK first — the kioskx build is signed with our key, so in-place update is refused. Install the kioskx APK.
- First contact: the board calls
getEquipmentwith its existing serial; the cloud auto-provisions it into the unclaimed pool and it boots to a working USD grid immediately (LM-25 verified). - Claim: operator registers the serial (console or
POST /api/v1/machines/register); any vends taken before claiming follow the machine (LM-05r verified). - Nayax: bind the reader's real Device Number
(
POST /machines/{no}/nayax) so settlement webhooks resolve (LM-06). Binding wires up the record; it does not give us the reader. See step 6. - What breaks / does not carry over: vendor-cloud order history, ad
configuration, and planogram do NOT migrate (the vendor cloud is opaque
to us) — re-enter the planogram (LM-07) and restock counts. Everything
from first contact onward is continuous.
The Nayax terminal does not migrate either, and that half is not ours
to finish: on a second-hand cabinet the VPOS usually stays in the
previous operator's Nayax Core organisation, commissioned for their old
machine type. Its MDB profile and its settlement destination can only be
changed by that actor (or moved by Nayax support), so card can be dead —
or paying someone else — on a board that is otherwise fully migrated.
Scan & Pay (Stripe QR) has no such dependency and is the card path that
migrates cleanly. Details + the four ways to close it:
docs/payments-nayax.md→ "What migration cannot move". - Verify: cash vend + card vend that produces a real Nayax
transactionIdin your own payout report (a Kiosk-X-side settlement label is not proof) +ordersUpdatereconcile, then enroll the machine in the current OTA rollout.
Routed items (other in-flight agents own these)
| Failure / surface | Owner | State when verified |
|---|---|---|
Equipment config.info.moneySign ¥ server-side |
RCA/OTA currency agent | LANDED (31b4ad7) — eval hardened into a permanent regression gate (LM-08) |
| APK ¥/Chinese/vape-banner + OTA rollout to unit ...011 | RCA/OTA agent | In flight (reyeah repo dirty: localize gates, attract-banner overlay) |
| ZHZN one-APK universality + emulator harness verdict | ZHZN-universality agent | Verdict landed in the zhzn repo docs: one shared APK is NOT feasible — different board (T3568 vs RK3288), different wire protocol, no shared driver code; chosen path is the headless Python device agent against /zhzn/* (its "emulator harness" is a fake-VMC PTY, not an Android APK loop). Live enablement (fleet secret) still waits on its GO-LIVE doc; 4 commits unpushed |
| Product catalog imagery reseed (S3) | demo-imagery agent | LANDED (ad92acb — S3-hosted product shots for every seeded product) |
| Stripe checkout surfaces (ads self-serve, billing portal) | Stripe-audit agent | Simulated-checkout path verified; live keys are theirs to wire |
| Ad-chat session resilience | ad-chat agent | HMAC session-resume commit landed (280f1dd) |
| Funding-source enforcement (spec §4) | funding-enforcement agent | Suite test_funding_enforcement.py green (14 tests) |
| GameWorld rewards bridge | gameworld-bridge agent | Suite test_gameworld_bridge.py green; bridge fail-closed without secret (LM-22) |
| Operator web console deep parity | PDF-parity console eval loop (kiosk-x-operator repo) | Console reachable; API contracts it consumes covered here |
Blocked items and exactly what unblocks each
| Cell | Blocker | Unblocked by |
|---|---|---|
| Physical install, power-on, motor dispense, cash acceptor (rows 1, 2, 10, 19) | BLOCKED-HW | Site visit to demo unit 866903013700011 (or ...044) |
| Nayax reader tap-to-pay E2E (rows 6, 11) | BLOCKED-HW + BLOCKED-BIZ | Physical VPOS Touch + live Nayax merchant credentials & webhook secret |
| ID-scanner age-gate hardware (row 14) | BLOCKED-HW | AptPay/scanner calibration on the physical unit |
| ZHZN entire physical column | BLOCKED-HW | A ZHZN/CSM test unit (T3568 + Y-lift) |
| ~~ZHZN live gateway (row 3z live)~~ | UNBLOCKED 2026-08-12 | KIOSKX_ZHZN_DEVICE_SECRET CI-bootstrapped + wired (kube-infra b534dd65); live existing-zhzn PASS (run 8657773248). Read-back/rotate + per-device provisioning documented in kube-infra kiosk-x/secret-placeholder.yaml |
| Real programmatic DOOH revenue (row 21) | BLOCKED-BIZ | Vistar / Place Exchange / Blindspot / Adomni credentials |
| Live Stripe money movement (rows 12, 23) | BLOCKED-BIZ | Live Stripe keys in the kiosk-x-stripe secret (Stripe-audit agent) |
Eval-run ledger
Latest live run: 2026-08-11T21:07:42Z against https://api.kiosk-x.ai — BLOCKED-CFG 1, PASS 28
| Cell | Status | Evidence | ms |
|---|---|---|---|
| LM-00-health | PASS | health={'db': 'ok', 'redis': 'ok'}, readiness=pass, uptime=120s | 284.8 |
| LM-04-onboarding | PASS | operator lm-live-8648245569@evals.kiosk-x.ai created + signed in, empty fleet | 226.6 |
| LM-03r-autoprovision | PASS | unknown serial 866908648245570 booted to USD grid (10 lanes) | 156.7 |
| LM-03z-zhzn-live | BLOCKED-CFG | gateway disabled: KIOSKX_ZHZN_DEVICE_SECRET unset on live | 51.9 |
| LM-05r-claim-transfer | PASS | claimed 866908648245570; pre-claim vend transferred (totalOrders=1) | 310.0 |
| LM-06-nayax-bind | PASS | bound 017997981, signed webhook resolved + settled to owner | 336.0 |
| LM-07-planogram | PASS | operator edit visible on device planogram | 283.6 |
| LM-08-usd-pricing | PASS | public pricing catalog is USD-clean | 61.0 |
| LM-08-tax-estimate | PASS | tax-inclusive estimate 0.16 at rate 0.08875 | 504.0 |
| LM-08-equip-moneysign | PASS | equipment config.info.moneySign is $ | 190.3 |
| LM-09-restock | PASS | fill-all + per-aisle restock | 148.6 |
| LM-10r-cash-vend | PASS | cash vend booked $1.99 | 239.7 |
| LM-11r-card-vend-settle | PASS | card vend pending → webhook settled | 388.8 |
| LM-13r-cart | PASS | cart x2 billed $3.98 | 261.1 |
| LM-14-age-gate | PASS | Tobacco-21 gate: refused unverified, allowed verified | 294.0 |
| LM-16-webhook-idempotent | PASS | signed replay returned the same order (no dup) | 296.0 |
| LM-16-unsigned-webhook | PASS | unsigned webhook rejected 401 | 47.7 |
| LM-17r-reconcile-refund | PASS | MOTOR_JAM → needsRefund → refunded | 434.5 |
| LM-18-faults | PASS | fault visible to operator, cleared via aisle PUT | 297.8 |
| LM-19r-api-drop | PASS | api_drop enqueue → pending → ack → sale | 303.4 |
| LM-20r-ota-loop | PASS | publish → offer → install report → 100% adoption | 289.3 |
| LM-21-ad-loop | PASS | loop has 6 player-safe slots | 166.6 |
| LM-22-qr-surfaces | PASS | /play + /advertise render per machine | 99.8 |
| LM-23-subscriptions | PASS | subscribed (USD) then cancelled | 144.4 |
| LM-24-reports | PASS | orders/payments summaries + billing CSV | 146.3 |
| LM-25-migration | PASS | migrated serial 866908648245587: first contact → claim → build report | 202.7 |
| LM-26-decommission | PASS | decommissioned 866908648245588, reader 017962977 freed + rebound | 398.9 |
| LM-27-guides | PASS | matrix served on /guides/lifecycle-matrix | 68.7 |
| LM-02e-emu-lifecycle | PASS | 10-step walkthrough on fresh serial 866908222664731 | 587.3 |