All case studies
Case 04, AlertChainPublished3 min read

Building a reliable price-alert system

The engineering questionHow do scheduled data collection, threshold checks and notifications work together without duplicate or stale alerts?

System flow in AlertChain
  1. 01CoinGecko
  2. 02Scheduled fetch with Celery
  3. 03Snapshots in PostgreSQL
  4. 04Rule check: ABOVE or BELOW
  5. 05Email and in-app alert
Chart: Bitcoin price moving around a 75,000 alert line with scheduled checks. An alert is sent at the first crossing, nothing repeats during a 30-minute cooldown, and a second alert is sent at a new crossing after it.
Figure 04Scheduled checks run against fresh snapshots, and a cooldown makes one crossing send one alert.

01 Context

The problem

People set a target, look away and miss the moment the price gets there. AlertChain watches prices and tells them, so they don’t have to watch the chart. That sounds simple, but an alert system fails in three quiet ways: it alerts on old data, it alerts again and again while the price hovers around the target, or it loses the alert when an email fails to send.

  • Never evaluate a rule against a stale price.
  • One crossing should mean one alert, not a flood.
  • A failed email must not mean a lost alert.
  • Checks must run on schedule without anyone watching.

02 Design

The architecture and decisions

A scheduled Celery task fetches prices from CoinGecko for the coins people watch and stores a snapshot in PostgreSQL. A second step evaluates each rule, ABOVE or BELOW a target, against the latest snapshot. When a rule fires, it records when, and the notifications go out as their own tasks, by email through FastAPI Mail and in the app.

  • FastAPI and PostgreSQL for rules, snapshots and notifications
  • Celery for scheduled checks, with Redis as broker
  • CoinGecko for market data
  • Docker Compose to run the API, workers and scheduler together
ChoseScheduled polling
OverStreaming prices
Because

Target alerts don’t need tick-level data, and a schedule keeps API usage predictable and within rate limits.

ChoseStoring every snapshot
OverKeeping only the latest price
Because

History powers the charts, and it makes every alert explainable: you can see exactly which price triggered it.

ChoseA cooldown on each rule
OverOne-shot rules that must be re-created
Because

People keep their rules, and a price hovering around the line doesn’t turn into a stream of emails.

The check that keeps one crossing to one alert (simplified)python
def should_fire(rule, snap, now):
    if now - snap.taken_at > MAX_SNAPSHOT_AGE:
        return False  # stale: never evaluate old prices
    hit = (snap.price > rule.target if rule.direction == "ABOVE"
           else snap.price < rule.target)
    cooling = rule.last_fired_at and now - rule.last_fired_at < rule.cooldown
    return hit and not cooling

03 Implementation

The difficult parts

Scheduled market-data collection

Each run fetches all watched coins in one batched request instead of one call per rule, which keeps the number of API calls flat as people add rules. Each snapshot is stored with the time it was taken.

Stale prices

If a fetch fails or is delayed, the latest snapshot can be old. Every evaluation checks the snapshot’s age first, and a stale price is skipped rather than compared, because an alert based on old data is worse than a late one.

Duplicate alerts

When a rule fires, the time is saved in the same database transaction that creates the notification. During the cooldown, later checks see that time and stay quiet, so a price moving back and forth across the line still produces one alert.

Retries and delivery failures

The in-app notification is saved first, so it can’t be lost. Email is sent by a separate task that retries with backoff when the mail server fails, without re-running the rule check.

04 Validation

How I check it

Alert logic is easy to test because prices can be replayed. The checks use fixed price sequences and assert exactly which alerts should fire.

  • A clean crossing fires exactly one alert.
  • A price hovering and re-crossing during the cooldown fires nothing more.
  • A stale snapshot is never evaluated.
  • A failed email is retried, and the in-app notification still exists.

05 Reflection

What I learned and what comes next

Reliability is mostly about the boring cases

The happy path took a day. Stale data, repeats and failed deliveries are where the real design work was.

Store the evidence for every alert

Keeping snapshots made every alert explainable and made bugs much easier to find.

What comes next

An AI trading assistant that helps people understand market moves around their alerts, built on the same snapshot history.