GrahamScreener
13Alerts

Price Alerts

Overview

GrahamScreener sends email alerts when a stock hits a price condition you define. Alerts are evaluated hourly by a GitHub Actions workflow and delivered via Resend.

Condition Types

TypeTriggerExample
Target BuyPrice drops to or below thresholdAAPL at $165 — alert fires when price ≤ $165
Stop LossPrice drops to or below thresholdCBA.AX at $80 — alert fires when price ≤ $80
% Change UpPrice rises by X% from reference priceRELIANCE.BO reference $2,500, threshold 10% — fires at ≥ $2,750
% Change DownPrice drops by X% from reference priceINFY.NS reference $1,400, threshold 15% — fires at ≤ $1,190

For percentage-based alerts, you set a reference price at creation time. The alert evaluates against reference_price × (1 ± threshold/100).

How It Works

  1. You create an alert via the /alerts page — specify ticker, condition, threshold, and your email.
  2. GitHub Actions runs hourly (0 */1 * * *) — calls GET /api/cron/check-alerts on the live site with a Bearer CRON_SECRET header.
  3. The endpoint evaluates each active alert against the current price using a cache-first strategy: it reads from the snapshot_cache table (populated by GitHub Actions snapshots) and only falls back to Yahoo if the cache is missing or stale (>25 hours). If both fail, the alert is skipped that run — it won't crash the batch.
  4. If triggered, an email is sent via Resend and last_fired_at is updated.
  5. 24h debounce — once an alert fires, it won't fire again for 24 hours. This prevents spam during sustained price moves (e.g., a stock hovering around your stop loss for several days).

Cron Frequency

Alerts are checked once per hour via GitHub Actions (check-alerts.yml). The endpoint reads prices from the snapshot_cache table first (populated daily/weekly by snapshot workflows), falling back to Yahoo only when needed. This avoids Yahoo 429 rate-limit errors from Vercel's shared IP pool. Alerts are processed sequentially with 500ms pauses between tickers.

GitHub Actions has no cron frequency limits on public repos, so hourly execution works on any plan.

Migrating to Vercel Pro

If you upgrade to Vercel Pro ($20/mo), you can use Vercel's native cron instead of GitHub Actions:

  1. Add the cron block back to vercel.json:
    "crons": [{ "path": "/api/cron/check-alerts", "schedule": "0 */1 * * *" }]
    
  2. Disable the GitHub Actions workflow: Actions tab → "Hourly Alert Check" → "..." → "Disable workflow"
  3. Deploy — Vercel Pro runs the cron natively with lower latency

Resend Email Provider

GrahamScreener uses Resend for transactional email.

Free tier: 100 emails/day, 3,000/month. Sufficient for a personal tool with dozens of alerts.

When to upgrade: If you have > 100 alerts that could fire on the same day, or if you're running a multi-user deployment. Resend's $20/mo plan covers 50,000 emails/month.

Email Setup

Quick start (no DNS required)

Use Resend's built-in onboarding address:

RESEND_API_KEY=re_xxxxx
ALERT_FROM_EMAIL=onboarding@resend.dev

Emails will come from onboarding@resend.dev — fine for personal use.

Custom from-address (recommended for production)

  1. Sign up at resend.com and add your domain (e.g., grahamscreener.com).
  2. Add the DNS records Resend provides (DKIM, SPF, MX).
  3. Set env vars:
RESEND_API_KEY=re_xxxxx
ALERT_FROM_EMAIL=alerts@grahamscreener.com

Testing

RESEND_API_KEY=re_xxxxx ALERT_FROM_EMAIL=onboarding@resend.dev TEST_EMAIL=you@gmail.com npm run test-email

This sends a sample alert email (AAPL target buy at $165, current price $162.50) to verify your Resend setup works.

Environment Variables

VariableRequiredDefaultDescription
RESEND_API_KEYYes (for alerts)Resend API key (starts with re_)
ALERT_FROM_EMAILNoalerts@grahamscreener.comSender address (needs DNS setup unless using onboarding@resend.dev)
ALERT_REPLY_TONohello@grahamscreener.comReply-To address on alert emails — routes replies to support inbox
CRON_SECRETYes (for cron)Auth token for the check-alerts endpoint
TEST_EMAILNoYour email for npm run test-email

Privacy

Emails are stored only in the alerts table — one row per alert with the recipient address. No email logs are kept beyond Resend's standard retention. Alerts can be paused or deleted at any time from the /alerts page.

Database Schema

CREATE TABLE alerts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_email TEXT NOT NULL,
  ticker TEXT NOT NULL,
  exchange TEXT NOT NULL,
  condition_type TEXT NOT NULL CHECK (condition_type IN ('target_buy', 'stop_loss', 'pct_change_up', 'pct_change_down')),
  threshold REAL NOT NULL,
  active INTEGER NOT NULL DEFAULT 1,
  last_fired_at INTEGER,
  last_checked_at INTEGER,
  reference_price REAL,
  created_at INTEGER NOT NULL,
  notes TEXT
);

Last updated: 2026-05-10 by Claude Cowork