Inspiration We've all noticed prices at fast food places creeping up in ways that feel arbitrary: a burger costs different amounts depending on the app you order from, the time of day, whether there's a promo running. That got us joking around: what if a restaurant just... told you it was doing this? No pretending, no hiding it behind "market conditions." A kiosk that looks at your face, decides how much you're worth as a customer, and prices you accordingly, live, on screen.
The name came from that joke: McDynamic's, "I'm Pricing You™." It's satire, but the more we thought about it, the less far-fetched it felt. Surge pricing already exists everywhere from rideshares to concert tickets. Loyalty-based personalized pricing is a real thing retailers experiment with. We wanted to build the version of this that's honest about what it's doing, and use it as an excuse to build something with a real ML pricing brain behind it instead of just hardcoding a few "if loyal, charge more" rules. What it does McDynamic's is a self-order kiosk (running in the browser) that:
Detects your face using face-api.js the moment you step up Figures out if you're a returning customer or brand new, using face-distance matching against stored profiles Sends 20+ live signals about you and the market (loyalty tier, churn risk, price elasticity, time of day, season, holiday proximity, weather, demand, competitor pricing, and more) to our local model Gets back a price multiplier between 0.75x and 1.40x, along with the model's actual written reasoning for the number it picked Updates the whole menu instantly with the new prices, showing strikethrough base pricing and SAVE/SURGE badges Shows a full breakdown panel next to the kiosk so you can see exactly why your price is what it is: every signal, every factor, every line of the model's logic, nothing hidden Tracks a running "AI revenue impact" counter so you can see, order by order, how much the pricing model is making or costing the business
New customers get a discount to earn their loyalty. Loyal, low-churn regulars get priced up because the model knows they're not going anywhere. If someone's at risk of churning, the model can drop the price all the way down to try to win them back, overriding every other signal. If enough good signals stack up at once (loyal, low churn, peak demand, tight competition, a holiday coming up), the price gets pushed all the way to the 1.40x ceiling. We call that the "perfect storm" rule. How we built it The frontend is vanilla JavaScript using ES modules, no framework, just a single HTML page split into a kiosk panel and a debug/breakdown panel side by side. face.js runs face-api.js's TinyFaceDetector in a loop against the webcam feed, and does simple Euclidean distance matching (threshold 0.6) against face descriptors stored per-customer to decide if it's seeing someone new or someone it already knows. New faces get registered automatically through profiles.js, with an 8-second cooldown so the same person doesn't get re-registered five times while they're standing there.
The backend is Node.js with Express (server.js). It's responsible for computing all the raw signals (loyalty tier and score, churn risk, price elasticity, time slot, season, weather, demand, competitor pricing, session info) from whatever customer profile and slider values are active. Those signals get packaged into a prompt and sent to our local model through a /api/price endpoint. The prompt lays out a tiered pricing mandate: acquisition discounts for new customers, inelasticity pricing for loyal customers, a churn win-back override that beats everything else, and the compound "perfect storm" rule for when several strong signals line up at once. The model responds with structured JSON (multiplier, confidence, reasoning, strategy, dominant factor, risk flags), and the server clamps the multiplier into the 0.75 to 1.40 range as a safety check before sending it back.
On the frontend, pricing.js fetches that response and computeFactors() breaks it down for display, menu.js re-renders prices across the whole menu, and debug.js drives the entire right-hand breakdown panel: the price factor waterfall, loyalty score bars, churn risk badges, market context, and the model's reasoning paragraph. state.js holds everything shared across the app: the cart, the current multiplier, the event log, session revenue, so the UI stays in sync as things change.
We went all-in on our local model actually making the call. There's no simple formula backing it up or falling back on if the model call fails: if it doesn't return a valid multiplier, there's no price. That was a deliberate choice. We wanted the demo to be honest proof that the model was doing real reasoning over real signals, not window dressing on top of a spreadsheet. Challenges we ran into Getting face recognition to feel reliable in a browser with no backend face database was harder than expected. We're just comparing descriptor vectors client-side, so tuning the distance threshold to avoid false "new customer" registrations (or false matches between two different people) took a lot of trial and error under different lighting.
Prompt design for our local model was its own project. Early versions of the pricing prompt were too timid, multipliers barely moved off 1.0 no matter what we fed it. We had to be explicit about the tiered mandate (the acquisition discount range, the inelasticity capture range, the churn override, the perfect storm compound rule) before the model would actually commit to the extremes of the 0.75 to 1.40 range instead of playing it safe in the middle. Getting the "perfect storm" rule to reliably trigger only when 3+ conditions were genuinely true, and not just whenever the customer looked vaguely loyal, took several rounds of rewriting.
We also underestimated how much surface area there is in keeping ~20 signals, a cart, a multiplier, and a live event feed all in sync across two panels without a frontend framework. A few late-night bugs came from state getting updated in one panel but not reflected in the other. Accomplishments that we're proud of We're proud that the pricing is never faked. Every multiplier on screen came out of an actual call to our local model, reasoning over that specific customer's signals in that moment, and you can read exactly why in the breakdown panel. We're also proud of how aggressive we got the model to be: a genuinely loyal, low-churn VIP customer during a demand spike will hit the full 1.40x ceiling, and the reasoning behind it actually holds up when you read it. The face recognition "wow moment" (walking up as a stranger, then walking up again seconds later and being instantly recognized with your price already adjusted) works consistently, which we weren't sure we'd pull off in the time we had. What we learned We learned a lot about prompt engineering for structured, high-stakes numeric output. Getting a model to commit to a specific number inside explicit tiers, instead of hedging toward the middle, took real iteration and taught us a lot about how to write mandates instead of suggestions. We also got a lot more comfortable with face-api.js and the tradeoffs of doing face matching entirely client-side. And honestly, we learned some things about the psychology of pricing itself: building the "perfect storm" rule and watching it fire made the acquisition-vs-retention tension in dynamic pricing feel a lot more real than it did when it was just an abstract concept. What's next for McDynamics Persisting customer profiles and face descriptors server-side instead of localStorage, so recognition works across devices and sessions Swapping in real external data for the "market context" signals (actual weather API, actual competitor pricing feeds) instead of the simulated versions we used for the demo Letting the lightweight local ONNX model (currently loaded but unused) handle a cheap first-pass pricing estimate, with the larger local model called in only for harder or higher-stakes cases, to cut latency at scale A/B testing the pricing strategy against flat pricing to see whether the revenue uplift the demo counter shows actually holds up with real orders An "explain my price" button for customers themselves, not just the internal debug panel. If we're going to be honest about dynamic pricing, customers should get the same reasoning judges see
Built With
- css3
- es-modules
- express.js
- face-api.js
- html5
- javascript
- node.js
- onnx
- rest-api
- tinyfacedetector
Log in or sign up for Devpost to join the conversation.