Appearance
Smart Buylist โ customer intake surfaces (kiosk + storefront) โ
Date: 2026-07-14 Status: Approved direction (Brent, 2026-07-14) โ needs architect + /office-hours pass before implementation starts Related: #318 (Smart Buylist), #352 (kiosk surface decision), #350 (auth/tenant evolution), #326 (CollectorVision) POC: https://claude.ai/code/artifact/eb9374b0-2b15-4ce9-a8ec-fc0a71e311a3
Summary โ
Extend Smart Buylist (#318) beyond the merchant dashboard with a public, store-scoped intake surface so end customers can create buy orders themselves, either on an in-store kiosk (pinned iPad) or from the shop's website. One server-side quote engine serves every surface; the customer surfaces are submit-only and feed the merchant's existing review queue.
Approved decisions โ
- Hosted portal first, theme extension fast-follow. V1 is a hosted page (
buylist.lgsforge.com/{shop}or equivalent path) that merchants link from their site. A Shopify Theme App Extension + App Proxy version comes later for on-domain polish. POS UI Extension is out of scope for customer intake (staff-only surface; see #352). - The kiosk is the same hosted portal in kiosk mode (fullscreen PWA, device token, idle reset). This effectively resolves #352 in favor of its Option A for the buylist use case, without deciding the CollectorVision camera question.
- Instant quotes default ON, presented as "estimate, subject to review," with a per-store setting to turn them off (per surface if cheap to do).
- Email-only guest identity in v1. Shopify customer-account linking is deferred to the theme-extension phase.
Audience shift โ why new auth is needed โ
Both existing auth paths assume a known user: embedded mode needs a Shopify Admin session; standalone mode needs an LGS Forge User JWT. Customers have neither. Two new lightweight strategies are required, and both slot into the dualModeAuth โ named-strategies refactor proposed as phase 3 of #350:
- Kiosk device token โ merchant generates in Buylist Settings ("Add kiosk device"). Long-lived, revocable per device, scoped to intake-only endpoints for that one shop. This is the "device login" gap #352 names.
- App Proxy HMAC verification โ for the later theme-extension surface; verify Shopify's signed proxy requests (same shape as the existing
verifyWebhookmiddleware). - The hosted-portal web surface runs unauthenticated but store-scoped, protected by rate limits (below).
Architecture โ
One quote engine โ
List parsing โ catalog matching โ rate application lives in one service used by the merchant dashboard preview, the kiosk, and the web portal. Pricing extends the priceLookupService stage pipeline (raw โ modifier โ condition โ rounding) with a buylist-rate stage โ extend, don't duplicate (CLAUDE.md ยง5.9). Matching is plugin-driven via BaseGamePlugin; no if (game === ...) in the core (ยง rule 5). Matching runs against the global catalogs (mtg_sets, pokemon_sets, โฆ), not just synced Shopify*Product docs โ a customer can sell a card the store hasn't synced; accepting such a line can enqueue a single-card sync.
Public API namespace (server/routes/publicBuylist.js) โ
POST /public/buylist/:shop/quoteโ parse + match + price a list; returns the tentative offer only if the store's instant-quote setting allows.POST /public/buylist/:shop/ordersโ createBuylistOrder(status: 'pending',source: 'kiosk' | 'storefront'); returns order number + claim token.GET /public/buylist/orders/:id?token=โฆโ customer status page (offer sent โ accept/decline), claim-token-gated; no order enumeration.
All endpoints: Zod schemas in server/schemas/, validate() middleware, game explicit with no default (ยง5.5), aggressive per-IP + per-store rate limiting.
Data model โ
BuylistOrder (per-store collection, like sync_jobs):
shop,game,source: 'merchant' | 'kiosk' | 'storefront'customer: { email, name? },claimTokenHash,kioskDeviceId?lines[]: raw input line, match result (catalog UUID or unmatched), condition, qty, basis price snapshot, rule applied, offer- Status machine:
pending โ offer_sent โ accepted | declined โ paid - Every sub-document field declared field-by-field with a schema-shape round-trip test (ยง5.2).
Store.buylistConfig[gameId] (mirrors pricingConfig[gameId] resolution): price basis, rarity rates, bulk rate, stop-gap rate, condition multipliers, store-credit bonus %, prioritization toggles (out-of-stock boost, hot list), instant-quote visibility, kiosk devices list. Every field consumed in the same PR that adds it (ยง5.9).
Kiosk mode UX โ
Same portal page + kiosk flag with device token: fullscreen, large touch targets, idle-timeout reset to an attract screen, end-of-flow handoff ("You're order #1028 โ we'll call you") with optional QR to the customer's status page on their own phone. No customer data persists on the shared device between sessions.
Abuse / price-scraping mitigations โ
A public quote endpoint exposes buylist prices to competitors. Layers, cheapest first: per-IP and per-store rate limits (table stakes); daily quote-line caps per IP; optional Cloudflare Turnstile on the web surface (kiosk exempt via device token); merchant can disable instant quotes on the web surface while keeping them on the kiosk.
PR sequence (small PRs, per CLAUDE.md ยง3) โ
BuylistOrdermodel +Store.buylistConfig[gameId]+ settings UI- Quote engine service + merchant-side import/review (POC screens)
- Public API + hosted portal page (customer web surface live)
- Kiosk device auth + kiosk mode (attract screen, idle reset, QR handoff)
- Theme App Extension + App Proxy (on-domain storefront)
- Payout wiring (Shopify gift card via
shopifyAPI.js) + customer emails
Phases 1โ2 are pure #318; 3โ4 deliver kiosk + website; 5โ6 are fast-follows.
Open items before implementation โ
architectagent pass on theBuylistOrdermodel + public route/queue design;/office-hoursfor the auth-adjacent pieces (new public surface, device tokens).- Confirm subdomain/CORS line for the portal host (
server/index.jsallow-list โ #350 notes each new subdomain needs an explicit entry). - Decide gift-card issuance details at payout (customer account attach vs. anonymous code) during phase 6.
- Multi-game parity: portal + engine must work for MTG, Pokemon, and Riftbound from day one โ Riftbound gained store inventory sync on main (PRs #355โ357), so it resolves through the same plugin-driven path as the other games with no exemption needed (ยง5.1).
