Part 3 of 7 · Price monitor series ~5 min read

How a price move gets noticed

On each page’s schedule, an EventBridge Scheduler run fires the checker Lambda. The Lambda fetches the page politely, reads the price with that page’s saved rule, and writes the reading to history. Then the watcher looks at the new reading next to the last one, computes the change, and decides whether to stay quiet or to send an alert — and if so, which kind. The whole decision is plain Python. No model. No guessing. Every threshold lives in the rules doc, where a rep can edit it without a deploy.

Key takeaways

  • The checks run on a staggered schedule via EventBridge Scheduler — gentle, never a burst.
  • Each page has a move threshold in the rules doc — default 5%, with a bigger “big swing” band at 2× that.
  • Four moves per page, every check: steady, first alert, repeat move, big swing.
  • DynamoDB holds the last reading and last-alert state so the same move isn’t sent twice.
  • The watcher itself never calls a model. The decision is entirely deterministic.

The decision flow, per page

Decision flow per page on every scheduled check A vertical decision flow diagram. At the top, an input box "Reading from a page" with the new price, the page's product and competitor, the timestamp, and the page's DynamoDB state. Below that, a step "Compute percent change" — the new price compared against the last stored reading. Below that, a check "Page muted right now?" — if yes, route to "Steady" (do nothing this check). If no, continue. The next step "Look up move threshold" — pulls the per-page threshold from the rules doc; for example a 5 percent default with a big-swing band at twice that. The next step "Did it cross a threshold?" — compares the percent change against the threshold; if the change is below the threshold, route to "Steady." If it crossed the big-swing band or the stock state flipped, route to "Big swing." Otherwise read the DynamoDB state to see whether an alert has already gone out for this move. If no alert yet, route to "First alert" (or "Repeat move" if a previous alert in the same direction already went out). Each terminal box — Steady, First alert, Repeat move, Big swing — emits an event to EventBridge with the move and the page context. A note at the bottom: the rules doc holds every threshold; the watcher's code only enforces them — change a threshold in the doc and the next check uses the new value. Reading from a page product · competitor · price · time Step 1 Compute percent change new price vs last reading Step 2 Page muted right now? read DDB pm-mute table Step 3 Look up move threshold e.g. 5% move, 10% big swing Step 4 Did it cross a threshold? below → steady big band → big swing Step 5 Already alerted this move? read DDB pm-alerts table Steady do nothing First alert threshold crossed Repeat move moved again, same way Big swing large jump or stock flip if yes below big first again The rules doc holds every threshold — change one and the next check uses the new value.
Fig 3. The watcher’s decision tree, per page, per check. Five steps decide which of four moves applies. The rules doc holds every threshold; the watcher only enforces them.

Thresholds: 5% isn’t magic, it’s in the doc

The rules doc has one short section per product line. Each section names the threshold in plain prose: “Kettles: alert on any move of 5% or more; treat 10% or a stock change as a big swing. Premium blenders: alert on 3%; they barely move. Clearance lines: alert on 8%; they bounce around.” The first number is the move threshold — the smallest change worth an alert. The bigger number is the big-swing band — a jump large enough to flag as urgent the moment it happens.

The thresholds exist for a reason. A 5% move on a high-margin product is worth a glance. A 1% move is noise — rounding, a coupon, a regional tweak — and alerting on it just trains the owner to ignore the system. A big swing (a price halving, or a product going out of stock) usually means a real event — a clearance, a supply problem, a deliberate undercut — and deserves to jump the queue.

Per-page overrides exist too. The watch list sheet has an optional column called threshold_override. Type a percentage there and the watcher uses your number instead of the product-line default for that one row. This is the right escape hatch for the one product where even a 2% move matters because the margins are razor-thin.

Four moves, always

Every page, every check, lands in exactly one of four buckets. The names are simple on purpose.

  • Steady. The price didn’t move past the threshold, or the page is muted. Do nothing. Most pages, most checks, are steady.
  • First alert. The price crossed the move threshold and no alert has gone out for this move yet. Send a fresh alert with full context. Write a row to the pm-alerts DynamoDB table marking that this move has fired.
  • Repeat move. The price moved again in the same direction since the last alert. Send a follow-up that names the previous reading so the owner sees the trend, not a fresh surprise. Write the new alert to pm-alerts.
  • Big swing. A large jump — past the big-swing band — or a stock-state flip. Send it flagged as urgent, marked so dispatch can let it past the daily cap. A big swing usually means a real pricing event, so it’s one of the few cases worth interrupting an owner for.

State that makes the decision deterministic

The watcher reads two DynamoDB tables every check. pm-readings holds the price history: (page_id, ts) with the price, currency, and stock flag. pm-alerts records every alert that’s gone out: (page_id, move, alert_date, dispatched_via). With those two tables, the move-decision logic is a few dozen lines of Python and zero magic. A given page with a given new reading, a given threshold, and a given history always produces the same move. Re-running a check produces no extra alerts (because the state in DDB shows what already fired).

A muted page is an explicit row in a third table, pm-mute, with a mute_until timestamp. The watcher reads it in the “page muted?” check and treats the page as steady until the mute ends. Part 5 covers muting in detail.

Why the check uses no model

The watcher could call a model on each check to decide whether a move is “interesting”. It doesn’t. Two reasons. First, the check should be the one part of the system that is utterly predictable — if the rules doc says alert at 5% and the price moved 7%, the alert fires. A model in that loop introduces variance the team can’t reason about. Second, model calls cost money, and most pages most checks are steady, so the call would be wasted nine times out of ten.

Bedrock fires elsewhere — when a page layout changes and the saved rule stops finding the price (the re-read lane from Part 2), and on the monthly summary mentioned in Part 6. Not on the routine check. The watcher itself is plain Python that reads a number and compares it to history.

Next post: how an alert finds the right person, how quiet hours and a daily cap are honored, and what context rides along with every alert.

All posts