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
Target alerts don’t need tick-level data, and a schedule keeps API usage predictable and within rate limits.
History powers the charts, and it makes every alert explainable: you can see exactly which price triggered it.
People keep their rules, and a price hovering around the line doesn’t turn into a stream of emails.
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 cooling03 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.
