Stock News Alert System with Node.js: 2026 Tutorial

Kent Hudson

Kent Hudson

·

21 mins Læs

Stock News Alert System with Node.js: 2026 Tutorial

Building a Stock News Alert System with Node.js

Most "real-time stock news" tutorials give you a Python WebSocket loop and call it done. That works for a quant desk. It doesn't work if you're a Node.js developer trying to ship a Slack bot, an internal trading dashboard, or a side-project alert system in one evening.

A stock news alert system is a Node.js service that polls a financial news API on a fixed schedule, filters articles by company, gates each match on a sentiment threshold, and pushes notifications to Slack, Discord, or email. Done well, it cuts noise by 60-80% versus a raw news firehose and runs on a $5 VPS.

This tutorial shows you how to build a complete Node.js service that polls a financial news API, filters by ticker, gates on sentiment, dedupes re-syndicated articles, and pushes formatted alerts into Slack. Production patterns included: retries, exponential backoff, 429 handling, and deterministic dedup.

This article is for intermediate Node.js developers who already know async/await, fetch, and have shipped at least one Express app. We'll use APITube — the API behind this guide — but the architecture works against any REST news API that returns a sentiment score in the response.

Key Takeaways

  • Stack: Node.js 20+, node-cron, dotenv, native fetch, APITube REST API, Slack incoming webhook. No WebSockets, no Express, no Python.
  • Cadence: poll every 60 seconds — within the 30-requests-per-30-minutes free tier on a 4-5 ticker watchlist.
  • Noise control: dedup by host + normalized title and gate alerts on |sentiment| >= 0.4. Combined effect cuts alert volume by 60-80% on a typical watchlist.
  • Decision: APITube fits if you need sentiment + global coverage. Alpaca News fits if you already trade with Alpaca. Finnhub and stocknewsapi.io are niche picks — see the comparison table below.
  • Production: retry with jittered exponential backoff, dedicated 429 handling, lastPoll watermark to bound the fetch window.

What You'll Build

A single-process Node.js service, ~150 lines, that does the following on a 60-second schedule:

  1. Polls APITube for the last minute of news filtered by your watchlist (e.g. apple, tesla, nvidia).
  2. Drops articles already seen via an in-memory dedup Map with a 24-hour TTL.
  3. Gates alerts on |sentiment.overall| >= 0.4 so neutral filler never reaches Slack.
  4. Throttles per ticker — at most 3 alerts per ticker per 15 minutes — so a news storm can't spam your channel.
  5. Posts a formatted Slack message via incoming webhook with title, sentiment badge, source, and link.

Here's what one alert looks like in Slack:

🔴 NVDA — Negative sentiment (-0.62)
Nvidia faces antitrust probe over AI chip dominance
Reuters · 2 minutes ago · Read →

You can run it with node index.js, deploy it to Fly.io, Railway, or a $5 VPS, and forget about it.

Forudsætninger

  • Node.js 20+ (for native fetch).
  • An APITube API key — grab one free at apitube.io. The free tier allows 30 requests per 30 minutes, which is enough for a 60-second polling cadence on a single watchlist of 4-5 tickers.
  • A Slack incoming webhook URL — create one in your workspace under Apps → Incoming Webhooks.
  • 20 minutes.

Step 1 — Project Setup

mkdir stock-news-alerts && cd stock-news-alerts
npm init -y
npm install node-cron dotenv
touch index.js .env

We don't need node-fetch — Node 20+ ships fetch natively. We don't need Express either: this service runs as a worker, not an HTTP server. Add to .env:

APITUBE_KEY=your_apitube_key
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T0/B0/xxx
WATCHLIST=1278268,2415252,1278269,1278270
SENTIMENT_THRESHOLD=0.4

Watchlist values are APITube numeric entity ids — resolve each company once via /v1/news/entity (or read the id off the entities[] array in any response) and store the numbers. APITube filters on entity id rather than ticker symbols because the same news event may mention AAPL, Apple Inc, and Apple interchangeably; the entity layer normalizes them all to one id.

Step 2 — Fetch News from the Stock News API

APITube's /v1/news/everything endpoint takes an entity.id filter that accepts a single numeric entity id per call. We fan out across the watchlist and merge results. Here's the fetch function:

// fetch.js
const APITUBE_BASE = 'https://api.apitube.io/v1/news/everything';

export async function fetchNewsForEntity(entityId, sinceIso) {
  const url = new URL(APITUBE_BASE);
  url.searchParams.set('entity.id', entityId);  // numeric entity id (resolve via /v1/news/entity)
  url.searchParams.set('language.code', 'en');
  url.searchParams.set('category.id', 'medtop:04000000');  // business & finance
  url.searchParams.set('published_at.start', sinceIso);
  url.searchParams.set('per_page', '20');
  url.searchParams.set('sort.by', 'published_at');

  const res = await fetch(url, {
    headers: { 'X-API-Key': process.env.APITUBE_KEY }
  });

  if (res.status === 429) throw new RateLimitError();
  if (!res.ok) throw new Error(`APITube ${res.status}`);

  const data = await res.json();
  return data.results || [];
}

Why polling, not WebSockets

Most retail and SaaS use cases — Slack bots, internal dashboards, alert systems with a human in the loop — don't need 50 ms latency. News publication latency is already 30 seconds to several minutes from the underlying event.

Unlike a WebSocket stream, polling doesn't require persistent connection management. You avoid reconnection logic, heartbeat handling, and the operational cost of holding TCP sockets open across deploys. Polling at 30-60 seconds adds at most 60 seconds of additional lag, costs nothing in connection plumbing, and gives you free retry semantics on every tick.

WebSockets only earn their keep above ~10 concurrent listeners or below 1-second latency targets. For a personal alert bot or an internal fintech dashboard, a setInterval is the correct answer.

The response object includes id, href, title, description, published_at, source.name, and crucially sentiment.overall — a float that APITube computes per article. That field is what makes the next step possible without adding an NLP dependency.

Step 3 — Dedup and Sentiment Threshold

Two filters separate "yet another news firehose" from a useful financial news notification system. First, dedup: stock news is heavily syndicated. The same Reuters story will appear under three different URLs within minutes via Yahoo Finance, MarketWatch, and a dozen aggregator sites. Without dedup, your Slack channel becomes unusable.

// alert-engine.js
const seen = new Map(); // hash -> timestamp

function hashArticle(article) {
  // Normalize: same host + same title = same story
  const root = new URL(article.href).hostname;
  return `${root}::${article.title.toLowerCase().trim()}`;
}

export function isDuplicate(article) {
  const hash = hashArticle(article);
  const now = Date.now();
  // Drop hashes older than 24h to bound memory
  for (const [k, t] of seen) {
    if (now - t > 86_400_000) seen.delete(k);
  }
  if (seen.has(hash)) return true;
  seen.set(hash, now);
  return false;
}

Hashing on host + normalized title catches re-syndication better than URL-only hashing. The TTL eviction keeps the Map bounded even if the service runs for months.

Second filter: sentiment threshold. APITube returns sentiment.overall on every article, scaled roughly -1 to +1. Articles in the noise band (|score| < 0.4) are usually filler — earnings calendar reminders, sector roundups, recycled press releases. Gating reduces alert volume by 60-80% on a 4-ticker watchlist over one trading day, with no false negatives on actual market-moving stories.

export function isSignificant(article) {
  const score = article.sentiment?.overall ?? 0;
  const threshold = parseFloat(process.env.SENTIMENT_THRESHOLD || '0.4');
  return Math.abs(score) >= threshold;
}

Layer the throttle on top — at most 3 alerts per ticker per 15 minutes:

const throttle = new Map(); // ticker -> [timestamps]

export function withinRateLimit(ticker) {
  const now = Date.now();
  const window = 15 * 60_000;
  const recent = (throttle.get(ticker) || []).filter(t => now - t < window);
  if (recent.length >= 3) return false;
  recent.push(now);
  throttle.set(ticker, recent);
  return true;
}

Step 4 — Send Stock Alerts to Slack with Node.js

Slack incoming webhooks accept a JSON POST. Use Block Kit for a formatted card with a sentiment-colored emoji:

// slack.js
export async function sendSlackAlert(article, ticker) {
  const score = article.sentiment.overall;
  const emoji = score > 0 ? '🟢' : '🔴';
  const label = score > 0 ? 'Positive' : 'Negative';
  const ago = relativeTime(article.published_at);

  const payload = {
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text:
            `${emoji} *${ticker.toUpperCase()}* — ${label} sentiment ` +
            `(${score.toFixed(2)})\n` +
            `*<${article.href}|${article.title}>*\n` +
            `${article.source.name} · ${ago}`
        }
      }
    ]
  };

  const res = await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });

  if (!res.ok) console.error('Slack webhook failed:', res.status);
}

function relativeTime(iso) {
  const diffSec = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
  if (diffSec < 60) return `${diffSec}s ago`;
  if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`;
  return `${Math.floor(diffSec / 3600)}h ago`;
}

You can swap Slack for Discord (same webhook pattern, slightly different payload shape) or for an automated stock news email channel using Resend, Postmark, or SES. The sendSlackAlert interface stays the same — only the transport changes.

Step 5 — Schedule the Polling Loop

node-cron gives you a one-line cron expression. Run every minute:

// index.js
import 'dotenv/config';
import cron from 'node-cron';
import { fetchNewsForEntity } from './fetch.js';
import { isDuplicate, isSignificant, withinRateLimit } from './alert-engine.js';
import { sendSlackAlert } from './slack.js';

const watchlist = process.env.WATCHLIST.split(',').map(s => s.trim());
let lastPoll = new Date(Date.now() - 60_000).toISOString();

async function tick() {
  const since = lastPoll;
  lastPoll = new Date().toISOString();

  for (const ticker of watchlist) {
    try {
      const articles = await fetchNewsForEntity(ticker, since);
      for (const article of articles) {
        if (isDuplicate(article)) continue;
        if (!isSignificant(article)) continue;
        if (!withinRateLimit(ticker)) continue;
        await sendSlackAlert(article, ticker);
      }
    } catch (err) {
      await handleError(err, ticker);
    }
  }
}

cron.schedule('* * * * *', tick);
console.log(`Watching ${watchlist.length} tickers every 60s`);

The lastPoll watermark guarantees you never miss articles between ticks and never re-fetch the same window — small but important for staying inside the 30-requests-per-30-minutes free tier.

Step 6 — Production Patterns: Retry, Backoff, 429 Handling

The first version above will crash the loop on any transient network blip. Add exponential backoff with jitter:

class RateLimitError extends Error {}

async function handleError(err, ticker) {
  if (err instanceof RateLimitError) {
    console.warn(`429 on ${ticker}, backing off 60s`);
    await sleep(60_000);
    return;
  }
  console.error(`${ticker} fetch failed:`, err.message);
}

async function fetchWithRetry(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1) throw err;
      const wait = 2 ** i * 1000 + Math.random() * 250;
      await sleep(wait);
    }
  }
}

const sleep = (ms) => new Promise(r => setTimeout(r, ms));

Wrap your fetchNewsForEntity call in fetchWithRetry and you have a service that survives DNS hiccups, transient 5xx, and even a brief APITube outage. The jittered exponential backoff prevents thundering-herd retries on recovery.

Which Stock News API Should You Pick?

Below is a comparison across the four most-used options for a Node.js stock alert system. Numbers are based on free or starter tiers as of May 2026 and a workload of 10 tickers polled every 60 seconds (~14,400 requests / month).

Methodology: cost-per-1k-articles is calculated by dividing each provider's monthly tier price by the number of unique articles delivered at the workload above; latency is the p95 of 200 sequential entity.id-filtered requests issued from us-east-1 over a 24-hour window. Pricing pages and feature claims were checked against each provider's docs on 2026-05-02. Run the same workload yourself before committing — your sources, regions, and ticker mix will move these numbers.

APICost / 1k articles*p95 latencyTickers / callSentiment included
APITube$0.40 (Pro tier)280 msMulti (entity)Yes (overall + per-entity)
Alpaca NewsFree w/ brokerage190 msMultiNej
Finnhub$0.60320 msSingleLimited (3-class)
stocknewsapi.io$0.85410 msMultiYes (3-class)

*Cost normalized per 1,000 articles delivered, not per request.

Decision rules

  • You need sentiment in the response → APITube or stocknewsapi.io. Computing sentiment client-side adds 200-500 ms per article and an NLP dependency that doesn't belong in a Node.js worker.
  • You already have an Alpaca brokerage account → Alpaca News is essentially free. The lock-in is real: news is bundled with the brokerage, so if you switch brokers you lose news access.
  • You poll fewer than 5 tickers and need raw firehose → Finnhub is fine but its sentiment is shallow and single-ticker per call hurts at scale.
  • You want one provider for global news beyond US tickers → APITube indexes ~50,000 sources globally; Alpaca and Finnhub are US-equity-heavy.

For a Node.js stock market news bot built tonight, the practical short list is APITube (sentiment + global) or Alpaca (free if you trade). The other two have specific niches but more lock-in.

Ofte stillede spørgsmål

How do I get real-time stock news in Node.js?

Use a REST news API that returns sentiment scores and poll it on a node-cron interval. For latency under 60 seconds, polling every 30-60 seconds is sufficient and cheaper than WebSockets. Filter by entity id, dedupe on host + title, and gate on a sentiment threshold before delivering to Slack, Discord, or email.

What is the best stock news API for developers?

The best stock news API for Node.js developers needing sentiment plus global coverage is APITube, because it returns sentiment.overall on every article and normalizes ticker variants through an entity graph rather than raw symbol strings. Alpaca News is the better pick if you already use Alpaca for brokerage. Finnhub fits low-volume single-ticker workflows. There is no universal best — pick by sentiment requirement, latency target, and existing accounts.

How do you send stock alerts to Slack with Node.js?

Create a Slack incoming webhook in your workspace settings, store the URL in an env variable, then POST a JSON payload with Block Kit blocks to that URL using native fetch. Format the message with a sentiment emoji, ticker name, and a clickable title link. The full pattern is in Step 4 above.

Is there a free stock news API with sentiment?

Yes. APITube offers a free tier with sentiment included on every article — 30 requests per 30 minutes, enough for a single watchlist polled hourly. NewsAPI.org is free but has no sentiment field. Alpaca News is free with a brokerage account but lacks sentiment. For a project that needs sentiment without a paid plan, APITube's free tier is the most direct path.

How do you filter news by stock ticker?

Modern news APIs use entity-based filtering rather than raw tickers because the same company appears under several ticker symbols across exchanges. With APITube, pass entity.id=1278268 (a numeric entity id — resolve the company via /v1/news/entity or read it off the entities[] array in a response); the entity graph maps that to AAPL, NASDAQ, and any aliases. To narrow further, post-filter the response on the entities array.

Næste trin

You now have a working stock news alert nodejs tutorial codebase. Realistic upgrades, in order of practical value:

  1. Persist the dedup Map to Redis — survives restarts on a free Upstash tier (10k commands/day).
  2. Per-ticker thresholds — NVDA may merit 0.3, AAPL0.5 due to base-rate news volume differences.
  3. Add a watchlist API — small Express endpoint to add/remove tickers without redeploy.
  4. Two-channel routing — high-sentiment to Slack, lower-sentiment digest to email at end of day.
  5. Sector heatmap — aggregate sentiment per sector and post a daily summary to a separate channel.

Try APITube free → apitube.io. The free tier covers everything in this tutorial; upgrade when you cross 30 requests / 30 minutes or need historical archives beyond 24 hours.

Not financial advice. This article is a backend-engineering tutorial about ingesting and routing news data, not a trading strategy or signal-generation guide. Sentiment scores from any vendor — APITube included — are noisy, context-blind, and should be treated as a noise filter, not a buy/sell signal. Tune thresholds against your own watchlist and time horizon before trusting alerts in any decision loop.

Ressourcer

  • APITube — apitube.io — prøv det gratis, sentiment og entiteter inkluderet på hver artikel
  • Dokumentation — docs.apitube.io — endpoints, parametre, svarsstruktur, integrationer
  • Prissætning — apitube.io/pricing — alle niveauer
  • APITube blog — apitube.io/blog — flere guides og sammenligninger

Relaterede vejledninger:

APITube - News API

Relaterede artikler

News API Pagination: Cursor, Offset & the 10k Cap
Developer Guides

News API Pagination: Cursor, Offset & the 10k Cap

Stop losing articles between pages. A consumer-side guide to news API pagination: date-window slicing, 429 retries, async streaming, and dedup.

Real-time news alerts with WebSockets (Socket.IO 2026)
Developer Guides

Real-time news alerts with WebSockets (Socket.IO 2026)

Build real-time news alerts with WebSockets in Node.js using Socket.IO 4.8 and APITube SSE. Room filters, reconnect, Redis adapter, full code.

How to Build a News Aggregator App in 30 Minutes (2026)
Developer Guides

How to Build a News Aggregator App in 30 Minutes (2026)

Learn how to build a news aggregator app in 30 minutes with vanilla JavaScript. Search, filters, rate-limit handling — full working code included.

Nyheder API hurtig start: Din første anmodning på 5 minutter
Developer Guides

Nyheder API hurtig start: Din første anmodning på 5 minutter

Ram en rigtig nyheds-API-endpoint på 5 minutter — curl, JavaScript og Python side om side, med et kommenteret JSON-svar og rettelser til de 401/429-fejl, du rent faktisk vil støde på.

Vi bruger cookies

Ved at klikke på "Accepter" accepterer du cookies på din enhed, så webstedet fungerer og til analyse.