Implementing Price Alerts in Crypto Mobile App
Price Alerts — seemingly simple: user sets price, notification arrives when reached. Actually a real-time system with price stream, trigger logic, and guaranteed push delivery. Key architecture question — where to check conditions: server or client.
Server vs Client Checking
Client-side — app polls price in background and compares to threshold. Doesn't work: iOS kills background processes within minutes, Android without foreground service — same. Unsuitable for production.
Server-side — only right option. Server receives price stream, checks all user alerts, sends push on trigger. Client only creates/deletes alerts and receives notifications.
Price Stream: Data Sources
Options for real-time prices:
| Source | Protocol | Latency | Coverage |
|---|---|---|---|
| Binance WebSocket | WSS | < 100ms | All Binance trading pairs |
| CoinGecko API | REST polling | 30–60 sec | 10,000+ coins |
| CryptoCompare WebSocket | WSS | < 500ms | Exchange aggregation |
| Coinbase Advanced Trade | WSS | < 200ms | Coinbase pairs only |
For real-time alerts — WebSocket. For less urgent — polling.
Backend subscribes to Binance WebSocket:
// Node.js — price subscription via Binance WebSocket
const WebSocket = require('ws');
const PAIRS = ['btcusdt', 'ethusdt', 'solusdt'];
const ws = new WebSocket(`wss://stream.binance.com:9443/stream?streams=${PAIRS.map(p => p + '@ticker').join('/')}`);
ws.on('message', (data) => {
const { stream, data: ticker } = JSON.parse(data);
const symbol = stream.replace('@ticker', '').toUpperCase();
const price = parseFloat(ticker.c); // current price
priceCache.set(symbol, price);
alertEngine.checkAlerts(symbol, price);
});
Alert Engine
On each price update — check all active alerts for that pair:
class AlertEngine {
async checkAlerts(symbol: string, currentPrice: number): Promise<void> {
const alerts = await alertRepository.getActiveAlerts(symbol);
const triggered = alerts.filter(alert => {
if (alert.type === 'ABOVE') return currentPrice >= alert.targetPrice;
if (alert.type === 'BELOW') return currentPrice <= alert.targetPrice;
if (alert.type === 'PERCENT_CHANGE') {
const change = Math.abs((currentPrice - alert.basePrice) / alert.basePrice * 100);
return change >= alert.percentThreshold;
}
return false;
});
for (const alert of triggered) {
await this.fireAlert(alert, currentPrice);
}
}
private async fireAlert(alert: PriceAlert, price: number): Promise<void> {
// Deactivate alert to prevent duplicate notification
await alertRepository.deactivate(alert.id);
// Send push
await pushService.sendToUser(alert.userId, {
title: `${alert.symbol} reached ${formatPrice(price)}`,
body: this.buildAlertMessage(alert, price),
data: { screen: 'price_detail', symbol: alert.symbol }
});
// Save to history
await alertRepository.saveTriggeredAlert(alert, price);
}
}
Deactivation before push send — important. If push send fails and retry happens, alert already deactivated — no duplicates.
Mobile Client: Creating Alert
// iOS — alert creation form
struct CreateAlertView: View {
@State private var targetPrice: String = ""
@State private var alertType: AlertType = .above
let symbol: String
let currentPrice: Double
var body: some View {
Form {
Section("Condition") {
Picker("Alert type", selection: $alertType) {
Text("Price above").tag(AlertType.above)
Text("Price below").tag(AlertType.below)
Text("% Change").tag(AlertType.percentChange)
}
.pickerStyle(.segmented)
HStack {
Text("$")
TextField("0.00", text: $targetPrice)
.keyboardType(.decimalPad)
}
}
Section {
Text("Current price: \(formatPrice(currentPrice))")
.foregroundColor(.secondary)
}
Button("Create alert") {
createAlert()
}
.disabled(targetPrice.isEmpty)
}
}
}
Active Alerts List
User should see all alerts and manage them. Each alert — condition display, current price relative to threshold, delete button.
For proximity visualization — progress indicator with current price between base and target:
// Android — distance to threshold visualization
@Composable
fun AlertProgressBar(currentPrice: Double, targetPrice: Double, basePrice: Double) {
val progress = ((currentPrice - basePrice) / (targetPrice - basePrice)).coerceIn(0.0, 1.0)
LinearProgressIndicator(
progress = progress.toFloat(),
modifier = Modifier.fillMaxWidth(),
color = if (progress > 0.8) Color.Orange else MaterialTheme.colorScheme.primary
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(formatPrice(basePrice), style = MaterialTheme.typography.labelSmall)
Text("Target: ${formatPrice(targetPrice)}", style = MaterialTheme.typography.labelSmall)
}
}
Repeating Alerts
By default alert triggers once and deactivates. User can choose "repeat" — then alert reactivates N minutes after trigger to avoid spam during volatile market:
if (alert.isRepeating) {
const cooldownMs = alert.cooldownMinutes * 60 * 1000;
await alertRepository.scheduleReactivation(alert.id, Date.now() + cooldownMs);
}
Timeline
Implementing server alert engine with WebSocket price stream (Binance/CryptoCompare), mobile UI for creating/managing alerts, push on trigger with history — 8–12 working days.







