Guide for WebSocket, Server-Sent Events (SSE), real-time news updates, and configurable alerts.
- Overview
- Server-Sent Events (SSE)
- WebSocket Server
- Alert System
- Client Integration
- Push Notifications
- Webhooks
- Deployment
Free Crypto News supports multiple real-time delivery methods:
| Method | Best For | Supported Platforms |
|---|---|---|
| SSE | Simple streaming | Vercel, Cloudflare, all Edge |
| WebSocket | Bi-directional, subscriptions, alerts | Railway, Render, VPS |
| Alerts | Configurable conditions, webhooks | All platforms |
| Push | Mobile/browser notifications | All with service worker |
| Webhooks | Server-to-server | Any backend |
SSE is the recommended method for Vercel deployments.
GET /api/sse
| Parameter | Type | Default | Description |
|---|---|---|---|
sources |
string | all | Comma-separated source keys (e.g., coindesk,theblock) |
categories |
string | all | Comma-separated categories (e.g., bitcoin,defi) |
breaking |
boolean | true | Include breaking news events (false to disable) |
Example with filters:
curl -N "https://cryptocurrency.cv/api/sse?sources=coindesk,theblock&categories=bitcoin&breaking=true"| Event | Payload | Description |
|---|---|---|
connected |
{message, timestamp, config} |
Connection established |
news |
{type, articles[], timestamp} |
New articles available |
breaking |
{type, articles[], timestamp} |
Breaking news alerts |
heartbeat |
{timestamp} |
Keep-alive ping (every 30s) |
error |
{message, timestamp} |
Error notification |
JavaScript/Browser:
const eventSource = new EventSource('/api/sse');
// Breaking news
eventSource.addEventListener('breaking', (event) => {
const data = JSON.parse(event.data);
console.log('Breaking news:', data);
showNotification(data.title);
});
// Regular news updates
eventSource.addEventListener('news', (event) => {
const data = JSON.parse(event.data);
updateNewsFeed(data.articles);
});
// Connection status
eventSource.addEventListener('connected', (event) => {
console.log('Connected to real-time feed');
});
// Heartbeat (keep-alive)
eventSource.addEventListener('heartbeat', () => {
// Connection is alive
});
// Handle errors
eventSource.onerror = (error) => {
console.error('SSE error:', error);
// EventSource will auto-reconnect
};
// Clean up
window.addEventListener('beforeunload', () => {
eventSource.close();
});React Hook:
import { useEffect, useState } from 'react';
interface Article {
title: string;
link: string;
source: string;
pubDate: string;
}
export function useRealTimeNews() {
const [articles, setArticles] = useState<Article[]>([]);
const [breaking, setBreaking] = useState<Article | null>(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const eventSource = new EventSource('/api/sse');
eventSource.addEventListener('connected', () => {
setConnected(true);
});
eventSource.addEventListener('news', (event) => {
const data = JSON.parse(event.data);
setArticles(data.articles);
});
eventSource.addEventListener('breaking', (event) => {
const data = JSON.parse(event.data);
setBreaking(data);
// Clear after 30 seconds
setTimeout(() => setBreaking(null), 30000);
});
eventSource.onerror = () => {
setConnected(false);
};
return () => eventSource.close();
}, []);
return { articles, breaking, connected };
}Python Client:
import sseclient
import requests
import json
def stream_news():
url = 'https://cryptocurrency.cv/api/sse'
with requests.get(url, stream=True) as response:
client = sseclient.SSEClient(response)
for event in client.events():
if event.event == 'news':
articles = json.loads(event.data)
for article in articles.get('articles', []):
print(f"📰 {article['title']}")
elif event.event == 'breaking':
data = json.loads(event.data)
print(f"🚨 BREAKING: {data['title']}")
if __name__ == '__main__':
stream_news()Full bi-directional communication with subscriptions, live prices, whale alerts, and topic channels.
| Feature | Update Interval | Description |
|---|---|---|
| 📰 News Streaming | 30s | Real-time news from 130+ sources |
| 🚨 Breaking News | 30s | Urgent breaking news alerts |
| 💰 Price Streaming | 10s | Live prices (BTC, ETH, SOL, etc.) |
| 🐳 Whale Alerts | 60s | Large transactions ($1M+) |
| 😱 Fear & Greed | 5m | Market sentiment index |
| 📺 Topic Channels | Real-time | Bitcoin, DeFi, NFT, Regulation, etc. |
| 🔔 Custom Alerts | 30s | Your own alert rules |
| ⚡ Compression | - | WebSocket per-message deflate |
| 🛡️ Rate Limiting | - | 60 messages/minute protection |
The WebSocket server runs separately (for Railway, Render, VPS):
# Start WebSocket server
node ws-server.jsEnvironment Variables:
PORT=8080
NEWS_API=https://cryptocurrency.cv# Health check
curl http://localhost:8080/health
# Server statistics
curl http://localhost:8080/stats
# Available topic channels
curl http://localhost:8080/channels
# Current price cache
curl http://localhost:8080/pricesConnect:
const ws = new WebSocket('wss://your-ws-server.railway.app');
ws.onopen = () => {
console.log('Connected to WebSocket');
// Subscribe to news filters
ws.send(JSON.stringify({
type: 'subscribe',
payload: {
sources: ['coindesk', 'theblock'],
categories: ['defi', 'bitcoin'],
coins: ['BTC', 'ETH'],
keywords: ['SEC', 'ETF'],
}
}));
// Join topic channels
ws.send(JSON.stringify({
type: 'join_channel',
payload: { channel: 'bitcoin' }
}));
ws.send(JSON.stringify({
type: 'join_channel',
payload: { channel: 'defi' }
}));
// Enable live price streaming
ws.send(JSON.stringify({
type: 'stream_prices',
payload: { enabled: true }
}));
// Enable whale alerts
ws.send(JSON.stringify({
type: 'stream_whales',
payload: { enabled: true }
}));
// Enable Fear & Greed updates
ws.send(JSON.stringify({
type: 'stream_sentiment',
payload: { enabled: true }
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'news':
handleNewsUpdate(message.payload.articles);
break;
case 'breaking':
handleBreakingNews(message.payload.articles);
break;
case 'topic':
handleTopicNews(message.payload.channel, message.payload.articles);
break;
case 'prices':
handlePriceUpdate(message.payload.prices);
break;
case 'whales':
handleWhaleAlert(message.payload.alerts);
break;
case 'sentiment':
handleSentiment(message.payload);
break;
case 'alert':
handleAlert(message.data);
break;
}
};
ws.onclose = () => {
console.log('Disconnected, reconnecting...');
setTimeout(connect, 5000);
};Message Types:
| Type | Direction | Description |
|---|---|---|
subscribe |
Client → Server | Set news subscription filters |
unsubscribe |
Client → Server | Remove news filters |
join_channel |
Client → Server | Join a topic channel |
leave_channel |
Client → Server | Leave a topic channel |
stream_prices |
Client → Server | Enable/disable price streaming |
stream_whales |
Client → Server | Enable/disable whale alerts |
stream_sentiment |
Client → Server | Enable/disable sentiment updates |
subscribe_alerts |
Client → Server | Subscribe to custom alerts |
unsubscribe_alerts |
Client → Server | Unsubscribe from alerts |
ping |
Client → Server | Keep-alive |
news |
Server → Client | News articles |
breaking |
Server → Client | Breaking news |
topic |
Server → Client | Topic channel news |
prices |
Server → Client | Live price updates |
whales |
Server → Client | Whale transaction alerts |
sentiment |
Server → Client | Fear & Greed Index |
alert |
Server → Client | Custom alert events |
pong |
Server → Client | Keep-alive response |
rate_limited |
Server → Client | Rate limit warning |
| Channel | Name | Keywords |
|---|---|---|
bitcoin |
Bitcoin | bitcoin, btc, lightning, ordinals |
ethereum |
Ethereum | ethereum, eth, vitalik, layer2 |
defi |
DeFi | defi, yield, lending, dex, amm |
nft |
NFTs | nft, opensea, blur, digital art |
regulation |
Regulation | sec, regulation, cftc, lawsuit |
stablecoins |
Stablecoins | usdt, usdc, stablecoin, tether |
altcoins |
Altcoins | solana, cardano, polkadot, avalanche |
exchanges |
Exchanges | binance, coinbase, kraken, exchange |
markets |
Markets | price, rally, crash, bull, bear |
whales |
Whales | whale, accumulation, institutional |
{
"type": "prices",
"payload": {
"prices": {
"bitcoin": {
"usd": 98500,
"usd_24h_change": 2.5,
"usd_market_cap": 1950000000000
},
"ethereum": {
"usd": 3450,
"usd_24h_change": -1.2,
"usd_market_cap": 415000000000
}
}
},
"timestamp": "2026-02-02T10:00:00Z"
}{
"type": "whales",
"payload": {
"alerts": [
{
"hash": "0x123...",
"amount": 1500,
"symbol": "BTC",
"usd_value": 147750000,
"from": "Binance",
"to": "Unknown Wallet"
}
],
"isNew": true
},
"timestamp": "2026-02-02T10:00:00Z"
}{
"type": "sentiment",
"payload": {
"value": 72,
"classification": "Greed",
"timestamp": "2026-02-02T00:00:00Z"
},
"timestamp": "2026-02-02T10:00:00Z"
}{
"type": "subscribe",
"sources": ["coindesk", "theblock"],
"categories": ["defi", "bitcoin", "regulation"],
"coins": ["BTC", "ETH", "SOL"],
"keywords": ["SEC", "BlackRock", "ETF"]
}News Update:
{
"type": "news",
"articles": [
{
"title": "Bitcoin ETF Approval Expected",
"link": "https://...",
"source": "CoinDesk",
"pubDate": "2026-01-22T10:00:00Z",
"categories": ["bitcoin", "regulation"]
}
],
"timestamp": "2026-01-22T10:00:05Z"
}# Health check
curl https://your-ws-server.railway.app/health
# Statistics (includes alert subscribers)
curl https://your-ws-server.railway.app/statsConfigurable alerts for price movements, breaking news, and custom conditions with WebSocket and webhook delivery.
| Type | Description | Example |
|---|---|---|
price_above |
Price exceeds threshold | BTC > $100,000 |
price_below |
Price drops below threshold | ETH < $2,000 |
price_change_pct |
Percentage change in timeframe | SOL +10% in 24h |
volume_spike |
Volume exceeds multiplier of baseline | BTC volume 3x normal |
breaking_news |
Breaking news with optional keywords | News with "SEC", "ETF" |
ticker_mention |
Ticker mentioned with optional sentiment | BTC with sentiment > 0.5 |
whale_movement |
Large transfers above threshold | Transfers > $10M |
fear_greed_change |
Fear & Greed index change | Change >= 10 points |
curl -X POST https://cryptocurrency.cv/api/alerts \
-H "Content-Type: application/json" \
-d '{
"name": "BTC Above 100k",
"condition": {
"type": "price_above",
"coin": "bitcoin",
"threshold": 100000
},
"channels": ["websocket", "webhook"],
"webhookUrl": "https://your-server.com/alerts",
"cooldown": 300
}'Response:
{
"alert": {
"id": "alert_1737507600_abc123def",
"name": "BTC Above 100k",
"condition": {
"type": "price_above",
"coin": "bitcoin",
"threshold": 100000
},
"channels": ["websocket", "webhook"],
"webhookUrl": "https://your-server.com/alerts",
"cooldown": 300,
"enabled": true,
"createdAt": "2026-01-22T00:00:00.000Z"
}
}Price Above:
{
"type": "price_above",
"coin": "bitcoin",
"threshold": 100000
}Price Change Percentage:
{
"type": "price_change_pct",
"coin": "ethereum",
"threshold": 10,
"timeframe": "24h"
}Volume Spike:
{
"type": "volume_spike",
"coin": "solana",
"multiplier": 3
}Breaking News with Keywords:
{
"type": "breaking_news",
"keywords": ["SEC", "ETF", "regulation"]
}Ticker Mention with Sentiment:
{
"type": "ticker_mention",
"ticker": "BTC",
"minSentiment": 0.5
}Fear & Greed Change:
{
"type": "fear_greed_change",
"threshold": 10
}| Method | Endpoint | Description |
|---|---|---|
| GET | /api/alerts |
List all alert rules |
| POST | /api/alerts |
Create alert rule |
| GET | /api/alerts?action=evaluate |
Trigger alert evaluation |
| GET | /api/alerts?action=stats |
Get alert statistics |
| GET | /api/alerts?action=events |
Get recent alert events |
| GET | /api/alerts/[id] |
Get single alert |
| PUT | /api/alerts/[id] |
Update alert |
| PATCH | /api/alerts/[id] |
Enable/disable alert |
| DELETE | /api/alerts/[id] |
Delete alert |
| POST | /api/alerts/[id]?action=test |
Test trigger alert |
const ws = new WebSocket('wss://your-ws-server.railway.app');
ws.onopen = () => {
// Subscribe to all alerts
ws.send(JSON.stringify({
type: 'subscribe_alerts',
payload: { ruleIds: ['*'] }
}));
// Or subscribe to specific alerts
ws.send(JSON.stringify({
type: 'subscribe_alerts',
payload: { ruleIds: ['alert_123', 'alert_456'] }
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'alert') {
const alertEvent = message.data;
console.log(`🚨 Alert: ${alertEvent.ruleName}`);
console.log(`Severity: ${alertEvent.severity}`);
console.log(`Value: ${alertEvent.data.currentValue}`);
}
};When an alert triggers, this event is broadcast:
{
"type": "alert",
"data": {
"id": "evt_1737507600_xyz789",
"ruleId": "alert_123",
"ruleName": "BTC Above 100k",
"condition": {
"type": "price_above",
"coin": "bitcoin",
"threshold": 100000
},
"triggeredAt": "2026-01-22T10:30:00.000Z",
"data": {
"currentValue": 105000,
"threshold": 100000,
"context": {
"coinId": "bitcoin",
"coinName": "Bitcoin",
"symbol": "btc",
"change24h": 5.2
}
},
"severity": "warning"
},
"timestamp": "2026-01-22T10:30:00.000Z"
}| Severity | Description |
|---|---|
critical |
Large deviation (>10% price, >5x volume) |
warning |
Moderate deviation (5-10% price, 3-5x volume) |
info |
Small deviation or informational |
Alert webhooks are sent as POST requests:
{
"type": "alert",
"event": {
"id": "evt_1737507600_xyz789",
"ruleId": "alert_123",
"ruleName": "BTC Above 100k",
"condition": { "type": "price_above", "coin": "bitcoin", "threshold": 100000 },
"triggeredAt": "2026-01-22T10:30:00.000Z",
"data": { "currentValue": 105000, "threshold": 100000 },
"severity": "warning"
},
"timestamp": "2026-01-22T10:30:00.000Z"
}Headers:
Content-Type: application/json
X-Alert-Event-Id: evt_1737507600_xyz789
X-Alert-Rule-Id: alert_123
Each alert has a configurable cooldown period (in seconds) to prevent spam:
- Default: 300 seconds (5 minutes)
- After triggering, the rule won't evaluate again until cooldown passes
- Test triggers (
?action=test) do not affect cooldown
import { useRealTimeNews } from '@/hooks/useRealTimeNews';
export function LiveNewsFeed() {
const { articles, breaking, connected } = useRealTimeNews();
return (
<div>
{/* Connection status */}
<div className={`status ${connected ? 'connected' : 'disconnected'}`}>
{connected ? '🟢 Live' : '🔴 Reconnecting...'}
</div>
{/* Breaking news banner */}
{breaking && (
<div className="breaking-banner">
🚨 BREAKING: {breaking.title}
</div>
)}
{/* News feed */}
<div className="news-feed">
{articles.map((article) => (
<ArticleCard key={article.link} article={article} />
))}
</div>
</div>
);
}<script>
class CryptoNewsStream {
constructor(options = {}) {
this.onNews = options.onNews || (() => {});
this.onBreaking = options.onBreaking || (() => {});
this.onConnect = options.onConnect || (() => {});
this.onDisconnect = options.onDisconnect || (() => {});
this.eventSource = null;
}
connect() {
this.eventSource = new EventSource('/api/sse');
this.eventSource.addEventListener('connected', () => {
this.onConnect();
});
this.eventSource.addEventListener('news', (e) => {
this.onNews(JSON.parse(e.data));
});
this.eventSource.addEventListener('breaking', (e) => {
this.onBreaking(JSON.parse(e.data));
});
this.eventSource.onerror = () => {
this.onDisconnect();
};
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
}
// Usage
const stream = new CryptoNewsStream({
onNews: (data) => {
console.log('New articles:', data.articles);
},
onBreaking: (article) => {
alert(`🚨 Breaking: ${article.title}`);
},
onConnect: () => {
document.getElementById('status').textContent = 'Live';
},
onDisconnect: () => {
document.getElementById('status').textContent = 'Reconnecting...';
},
});
stream.connect();
</script>Web Push for browser/mobile notifications even when the app is closed.
1. Get VAPID Keys:
# Generate keys (once)
npx web-push generate-vapid-keys2. Set Environment Variables:
VAPID_PUBLIC_KEY=BNx...
VAPID_PRIVATE_KEY=your-private-key
VAPID_SUBJECT=mailto:your-email@example.com// Check support
if ('serviceWorker' in navigator && 'PushManager' in window) {
// Get public key from server
const { publicKey } = await fetch('/api/push').then(r => r.json());
// Register service worker
const registration = await navigator.serviceWorker.register('/sw.js');
// Subscribe to push
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
// Send subscription to server
await fetch('/api/push', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'subscribe',
subscription,
filters: {
sources: ['coindesk'],
keywords: ['bitcoin'],
breakingOnly: false,
},
}),
});
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map(c => c.charCodeAt(0)));
}// sw.js
self.addEventListener('push', (event) => {
const data = event.data?.json() || {};
const options = {
body: data.body || 'New crypto news available',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
tag: data.tag || 'crypto-news',
data: { url: data.url || '/' },
actions: [
{ action: 'open', title: 'Read More' },
{ action: 'dismiss', title: 'Dismiss' },
],
};
event.waitUntil(
self.registration.showNotification(data.title || 'Crypto News', options)
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});Server-to-server notifications with HMAC signatures.
curl -X POST https://cryptocurrency.cv/api/webhooks \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhook",
"events": ["news.breaking", "news.new"],
"secret": "your-webhook-secret",
"filters": {
"sources": ["coindesk"],
"keywords": ["SEC", "ETF"]
}
}'| Event | Description |
|---|---|
news.new |
New article published |
news.breaking |
Breaking news detected |
price.alert |
Price threshold reached |
market.significant |
Major market movement |
system.health |
System status change |
{
"event": "news.breaking",
"timestamp": "2026-01-22T10:00:00Z",
"data": {
"article": {
"title": "SEC Approves Bitcoin ETF",
"link": "https://...",
"source": "CoinDesk",
"pubDate": "2026-01-22T09:58:00Z"
}
}
}const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}
// Express middleware
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
switch (event) {
case 'news.breaking':
handleBreakingNews(data.article);
break;
case 'news.new':
handleNewArticle(data.article);
break;
}
res.status(200).send('OK');
});Python:
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, f'sha256={expected}')SSE works out of the box on Vercel Edge Runtime:
// src/app/api/sse/route.ts
export const runtime = 'edge';# railway.toml
[build]
builder = "NIXPACKS"
[deploy]
startCommand = "node ws-server.js"
healthcheckPath = "/health"
healthcheckTimeout = 10
[[services]]
name = "websocket"
port = 8080# render.yaml
services:
- type: web
name: crypto-news-ws
env: node
plan: starter
buildCommand: npm install
startCommand: node ws-server.js
healthCheckPath: /healthFROM node:20-alpine
WORKDIR /app
COPY ws-server.js package.json ./
RUN npm install ws
EXPOSE 8080
CMD ["node", "ws-server.js"]docker build -t crypto-news-ws .
docker run -p 8080:8080 -e NEWS_API_URL=https://cryptocurrency.cv crypto-news-ws┌─────────────────────────────────────────────────────────────┐
│ Clients │
├─────────────────┬─────────────────┬─────────────────────────┤
│ Browser SSE │ WebSocket │ Push Subscribers │
│ (EventSource) │ (Full-duplex) │ (Service Workers) │
└────────┬────────┴────────┬────────┴────────────┬────────────┘
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌───────────────┐ ┌──────────────────────┐
│ /api/sse │ │ ws-server.js │ │ /api/push │
│ (Edge Runtime) │ │ (Standalone) │ │ (Web Push) │
└────────┬───────┘ └───────┬───────┘ └──────────┬───────────┘
│ │ │
└─────────────────┴─────────────────────┘
│
▼
┌──────────────────┐
│ News Sources │
│ (RSS Feeds) │
└──────────────────┘
| Method | Limit |
|---|---|
| SSE | Unlimited connections |
| WebSocket | 1000 concurrent |
| Push | 10,000 subscribers |
| Webhooks | 100 per account |