The problem: alert fatigue
Keeping up with crypto meant juggling five different apps — a price tracker, a scanner, news feeds, sentiment charts, and a stack of alert apps. And even then I kept missing the moves that mattered.
The existing tools fell into two camps:
- Too noisy — thousands of alerts, most of them irrelevant.
- Too shallow — pretty charts without any real analysis behind them.
So I built SignalWatch: a mobile app that turns overwhelming market data into a clear, prioritized signal board. It scans 100+ coins, understands plain-English questions, tracks market sentiment, and only alerts you about assets that actually pass the bar.
The stack
| Layer | Technology |
|---|---|
| Mobile | React Native (Expo SDK 54), TypeScript, React Navigation 7 |
| Auth & Sync | Supabase (Google OAuth + email/password) |
| Analysis engine | Node/Express on Cloud Run — scheduled every 15 minutes |
| Data ingestion | Cloudflare Workers (CoinMarketCap + Binance) |
| AI semantic scanner | Cloudflare Workers AI |
| Push notifications | FCM via a Supabase Edge Function |
| Market data | Binance & CoinMarketCap |
Architecture
┌─────────────────────────────────────────────┐
│ React Native App (Expo) │
│ Dashboard · Scanner · Signal · Calendar │
└───────┬──────────────────┬──────────────────┘
│ Supabase queries │ POST /api/ai-query (Bearer JWT)
▼ ▼
┌────────────┐ ┌──────────────────┐
│ Supabase │ │ Cloudflare Worker│
│ (Postgres)│ │ Workers AI │
└─────▲──────┘ └────────▲─────────┘
│ │
┌───────┴────────┐ ┌───────┴────────┐
│ Cloud Run │ │ Cloudflare │
│ (analysis, │ │ Worker │
│ every 15min) │ │ (CMC ingest) │
└───────▲────────┘ └───────▲────────┘
│ │
└──── Binance + CoinMarketCap ────┘
Two services feed a shared Supabase database:
- Cloud Run backend — technical analysis (RSI, MACD, EMA, Bollinger Bands, SuperTrend) across futures & spot markets, plus economic calendar events.
- Cloudflare Worker — CoinMarketCap ingestion (top 100, trending, fear & greed, news) and the AI query endpoint.
The app itself is architected with thin screens over a central hook (useDashboard) that acts as the single source of truth, with a clean service layer for all data access.
The feature I'm most proud of: the AI semantic scanner
Users can ask questions in plain English:
"show me coins with RSI < 30 and volume spike > 100%"
The query goes to a Cloudflare Worker, which detects intent, pulls real data from Supabase, and lets Workers AI compose a structured, data-backed answer. No hardcoded results, no fake confidence scores.
One subtle but critical detail: the app authenticates to the worker with the user's Supabase access token (Bearer), and the worker validates it against the Supabase JWKS. No secrets are ever embedded in the app binary.
// src/services/ai.ts — get a valid token, then call the worker
const getValidAccessToken = async (): Promise<string | null> => {
const { data: userData } = await supabase.auth.getUser();
if (userData?.user) {
const { data: { session: refreshed } } = await supabase.auth.getSession();
return refreshed?.access_token ?? null;
}
const { data: refreshData, error: refreshError } =
await supabase.auth.refreshSession();
if (!refreshError && refreshData.session?.access_token) {
return refreshData.session.access_token;
}
return null;
};
⚠️ The ES256 vs RS256 gotcha
This project taught me a lesson: not all Supabase JWTs are RS256. This particular project signs tokens with ES256 (EC P-256). My worker's JWT verifier originally only handled RS256, so every valid token was rejected with a confusing 401 — "Please sign in" — even though the user was clearly signed in.
The fix was to support both ES256 and RS256 in the worker's verification path, plus logging exactly why verification failed (much easier to debug than a generic 401).
Lessons learned along the way
1. External sites will block your cloud IPs
ForexFactory (for the economic calendar's news feed) returns HTTP 403 to Cloud Run datacenter IPs. The calendar HTML page works fine, but the news scrape doesn't. My solution: a graceful fallback — if the forex_news table is empty or the request fails, the app falls back to CoinMarketCap news (news_insights), which is populated by the Cloudflare Worker. The section is never empty, and users can't tell the difference.
// src/services/marketCalendar.ts — fallback to CMC news when forex news fails
export const fetchForexNews = async (limit = 10): Promise<ForexNewsRow[]> => {
const { data, error } = await supabase
.from('forex_news').select('*')
.order('published_at', { ascending: false })
.limit(limit);
const forexNews = (data || []) as ForexNewsRow[];
if (!error && forexNews.length > 0) return forexNews;
// ...fallback to news_insights (CoinMarketCap via worker)
};
2. Data freshness is a feature
A pipeline that ingests every 15 minutes across multiple timeframes (15m / 1h / 4h / 1d) means the "now" is actually now. That matters enormously in fast-moving markets — and it's a real competitive differentiator, not a buzzword.
3. Alert fatigue is the real enemy
Most tools push volume, not signal. SignalWatch bakes in quality filters so users only get notified about assets that actually pass the bar. It sounds obvious, but it's the thing that makes the product feel calm instead of chaotic.
4. Keep the app architecture boring
Thin screens → one central hook → a service layer → Supabase/worker. It kept a solo developer sane, made the codebase easy to grow, and made the light/dark theme + EN/ID localization trivial to add.
The result
- 🎯 AI scanner you can talk to (Cloudflare Workers AI over live data)
- 📊 Multi-timeframe technical analysis on 100+ coins
- 🌍 Global sentiment (Fear & Greed, bullish ratio, market score)
- 🔔 Quality-filtered push notifications (no spam)
- ⭐ Synced watchlist, 📅 economic calendar, 📰 impact-labeled news
- 🌗 Light/dark + EN/ID, fully customizable
Currently available on Google Play with all Pro features free. iOS is next (the codebase is Expo, so the path is clear).
If you've hit similar walls with external APIs blocking cloud IPs, or JWT algorithms that don't match the docs — I'd love to hear how you solved it. Feedback and feature ideas are very welcome. 👇
Links: Google Play
Top comments (0)