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

How a price alert reaches the owner

The watcher picked a move — first alert, repeat move, or big swing. Now the dispatch Lambda has to figure out who to send it to, on what channel, at what time of day, and with what context attached. Get any of those wrong and the alert is worse than no alert: a 2am Slack ping, a bare “a price changed,” a flood of twenty alerts during a competitor’s sale day. Four small guardrails sit between the move and the actual alert.

Key takeaways

  • Owner resolution: per-page override beats per-product-line default beats fallback to the configured admin.
  • Slack DMs are the default; email is the fallback if no Slack ID is configured.
  • Quiet hours defer alerts to the next business hour; a daily cap stops a sale day from burying an owner.
  • Every alert ships with the product, competitor, old and new price, the size of the move, recent history, and your own price.
  • A big swing is allowed past the daily cap; the system never edits a price either way.

Four guardrails on every dispatch

Four guardrails between the watcher's chosen move and the dispatched alert A horizontal flow diagram. On the far left, a "Move chosen" box: the watcher emitted an event with the page, the product line, and one of three sending moves — first alert, repeat move, or big swing. Four guardrail gates sit in a row to the right, each drawn as a vertical bar. Gate 1: Resolve owner — looks up the per-page owner column first; if blank, falls back to the per-product-line owner from the rules doc; if still blank, falls back to the configured admin. Returns a Slack member ID if one is set, otherwise an email address. Gate 2: Quiet hours — checks the local time against the rules-doc quiet-hours window (default 7pm to 8am); if outside business hours, defers the dispatch to the next available business minute via an EventBridge Scheduler one-off rule. Gate 3: Daily cap — checks how many alerts have already gone to this owner today against the per-owner cap; if the cap is hit, the alert is rolled into the daily digest instead of sent now, except a big swing which is always allowed through. Gate 4: Final compose — formats the alert message from the voice doc template for the chosen move, attaches the page context (product, competitor, old and new price, the percent move, a short recent history, your own listed price, a link to the page), adds a Snooze button. After all four gates pass, the alert ships via Slack for the resolved Slack ID, or via SES outbound for the resolved email. A note at the bottom: every dispatch is logged to DynamoDB so the next check knows the move has already gone out. Move chosen first alert, repeat, or big swing Gate 1 Resolve owner page override? line default? admin fallback? prefer Slack ID, fall back to email Gate 2 Quiet hours local time in business window? if not, defer via Scheduler Gate 3 Daily cap hit owner’s cap today? if yes, roll into digest (swing exempt) Gate 4 Compose + context voice template for the move + prices, move, history, link, Snooze button Dispatch — Slack DM (preferred) or SES outbound email (fallback) chat.postMessage for Slack · SES SendRawEmail for inbox every dispatch logged to DDB pm-alerts — the next check won’t duplicate Every gate is a deterministic check — no model calls, no surprise behavior on a busy sale day.
Fig 4. Four guardrails between the move and the dispatched alert. Resolve the owner. Honor quiet hours. Respect the daily cap. Compose with full context. Then ship via Slack or email and log the dispatch so the next check doesn’t duplicate.

Gate 1: resolve the owner

Three places the dispatch Lambda looks for the owner of a page, in order. First, the watch list’s per-page owner_email column — if a row has a specific person assigned, that person owns it regardless of the product-line default. Second, the per-product-line default in the rules doc (“all kettle pages default to Priya”). Third, the configured admin fallback — the person who set up the monitor and gets every unowned alert. The fallback should never fire in steady state; if it does, the weekly digest names every page that hit the fallback so the rules doc can be updated.

Once the dispatch knows which person to alert, it looks up their delivery preference. The voice doc maps each owner to a Slack member ID if one is set, otherwise to an email address. Slack is preferred because price moves feel like work-context messages, and a Slack DM with a Snooze button is more useful than an email link. Email is the fallback so nobody falls through the cracks.

Gate 2: quiet hours

The checks are staggered through the day, so an alert can land at any hour — including outside business hours. A price that drops at 11pm shouldn’t buzz an owner’s phone at 11pm; it can wait until morning, because nobody’s changing a price overnight anyway.

Gate 2 reads the rules doc’s quiet-hours setting (default 7pm to 8am, configurable per business). If the current local time is in the quiet window, the dispatch creates a one-off EventBridge Scheduler rule that fires at the next business-hour minute and exits without sending. The Scheduler invokes the same dispatch Lambda with the same payload at the deferred time, where Gate 2 will let it through. The one exception is a big swing flagged urgent, which the rules doc can allow to bypass quiet hours if the business wants to know the moment a competitor halves a price.

Gate 3: daily cap

A competitor running a storewide sale can move twenty of their products in an hour. Without a cap, that’s twenty Slack DMs in an owner’s morning — the fastest way to teach someone to mute the whole system. Gate 3 counts how many alerts have already gone to this owner today against a per-owner cap (default eight). Once the cap is hit, further first-alert and repeat-move alerts are rolled into that owner’s daily digest instead of sent live: a single message listing everything that moved, sent at a set time.

A big swing is exempt. The whole point of the big-swing band is “this is unusual enough to interrupt for,” so it always goes through, cap or not. The cap shapes the ordinary noise without hiding the rare events that matter.

Gate 4: compose with full context, then ship

The voice doc has one Slack message template per move: a short message with placeholders for the product name, competitor, old and new price, the percent move, a short recent history, your own listed price, and a link to the competitor page. The dispatch Lambda fills the placeholders, attaches a “Snooze” button (covered in Part 5), and ships the message via the Slack chat.postMessage API. The Slack token itself lives in Secrets Manager.

For email fallback, the same template is wrapped in a small HTML email with the same fields and a link that, when clicked, hits a Function URL that records a snooze or a note — the email equivalent of the Slack buttons.

The recent history is what makes a price alert worth reading. “Competitor Acme dropped the kettle 13%” is useful; “...and this is the third cut in two weeks, the price has gone $79 → $74 → $69” tells the owner whether this is a one-off promo or a sustained campaign. The dispatch pulls the last several readings from pm-readings and renders them as a tiny text sparkline so the trend is visible at a glance.

Every dispatch — Slack or email, any move — writes a row to pm-alerts in DynamoDB. The next check reads that row and knows not to alert the same move again.

Why the guardrails exist

None of these gates are exotic. They’re the kind of small care a thoughtful person would take if they were sending the alerts themselves — check who actually owns this product, don’t ping at 11pm, don’t send twenty messages during one sale, include enough context that the recipient can decide without opening five tabs. Putting them in code as four small sequential gates makes them part of the design, not something you’re trusting any one alert to remember. And note what no gate does: none of them touch a price. The system’s job ends at telling the owner clearly.

Next post: how a price alert gets handled once it lands — the three actions an owner can take, and why none of them ever change your own prices.

All posts