Build an Insider Trading Discord Bot in Node.js

Academic research shows insider cluster buys — multiple executives purchasing shares within days of each other — predict 12-month abnormal returns of 7-13% (Lakonishok & Lee, 2001). This tutorial builds a Discord bot that detects them automatically using free SEC Form 4 data.

~15 min read · Intermediate Node.js

What Are Insider Cluster Buys?

When a single insider buys stock, it's a data point. When 3+ insiders buy within a 2-week window, it's a signal. These "cluster buys" suggest insiders collectively believe the stock is undervalued.

The Insider Trading API provides normalized Form 4 data: transaction type (buy/sell/option exercise), dollar amounts, insider role (CEO/CFO/Director), and filing dates — everything you need to detect clusters.

Step 1: Set Up the Project

mkdir insider-bot && cd insider-bot
npm init -y
npm install discord.js node-fetch@2

# Create .env (never commit this!)
echo "DISCORD_TOKEN=your_bot_token_here" > .env
echo "CHANNEL_ID=your_channel_id_here" >> .env

Step 2: Build the Cluster Detector

// cluster.js — Detects insider cluster buys
const fetch = require('node-fetch');

const CLUSTER_WINDOW_DAYS = 14;
const MIN_INSIDERS = 3;

async function detectClusters(ticker) {
  const url = `https://securitiesdb.com/api/v1/stocks/${ticker}/insider-trading`;
  const res = await fetch(url);
  if (!res.ok) return null;

  const { data } = await res.json();
  const buys = data.transactions
    .filter(t => t.type === 'purchase' && t.value > 10000)
    .sort((a, b) => new Date(b.date) - new Date(a.date));

  // Sliding window: find groups of 3+ buys within 14 days
  for (let i = 0; i < buys.length - MIN_INSIDERS + 1; i++) {
    const window = [buys[i]];
    for (let j = i + 1; j < buys.length; j++) {
      const daysDiff = (new Date(buys[i].date) - new Date(buys[j].date))
                       / (1000 * 60 * 60 * 24);
      if (daysDiff <= CLUSTER_WINDOW_DAYS) window.push(buys[j]);
    }

    if (window.length >= MIN_INSIDERS) {
      const uniqueInsiders = new Set(window.map(t => t.insider_name));
      if (uniqueInsiders.size >= MIN_INSIDERS) {
        return {
          ticker,
          cluster_size: uniqueInsiders.size,
          total_value: window.reduce((s, t) => s + t.value, 0),
          window_start: window[window.length - 1].date,
          window_end: window[0].date,
          insiders: [...uniqueInsiders],
        };
      }
    }
  }
  return null;
}

module.exports = { detectClusters };

Step 3: Wire Up the Discord Bot

// bot.js — Discord bot that scans for insider clusters
require('dotenv').config();
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
const { detectClusters } = require('./cluster');

const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
});

// Watchlist — expand as needed
const WATCHLIST = [
  "AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "NVDA",
  "JPM", "BAC", "GS", "V", "MA", "UNH", "JNJ", "PFE",
];

async function scanAndAlert() {
  const channel = await client.channels.fetch(process.env.CHANNEL_ID);

  for (const ticker of WATCHLIST) {
    const cluster = await detectClusters(ticker);
    if (!cluster) continue;

    const embed = new EmbedBuilder()
      .setColor(0x00ff00)
      .setTitle(`🔔 Insider Cluster Buy: ${cluster.ticker}`)
      .setDescription(
        `**${cluster.cluster_size} insiders** bought within ${
          Math.ceil((new Date(cluster.window_end) - new Date(cluster.window_start))
          / (1000*60*60*24))
        } days`
      )
      .addFields(
        { name: 'Total Value', value: `$${(cluster.total_value).toLocaleString()}`, inline: true },
        { name: 'Window', value: `${cluster.window_start} → ${cluster.window_end}`, inline: true },
        { name: 'Insiders', value: cluster.insiders.join(', ') },
      )
      .setFooter({ text: 'Data: SecuritiesDB Free API · SEC Form 4' });

    await channel.send({ embeds: [embed] });
    // Respect rate limits
    await new Promise(r => setTimeout(r, 1000));
  }
}

client.on('ready', () => {
  console.log(`Bot online as ${client.user.tag}`);
  scanAndAlert(); // Run immediately
  setInterval(scanAndAlert, 6 * 60 * 60 * 1000); // Then every 6 hours
});

client.login(process.env.DISCORD_TOKEN);

Step 4: Combine with Quant Scores

A cluster buy is more meaningful when the company is financially healthy. Cross-reference with quant scores:

// Enhance alerts with quant health context
async function enrichCluster(cluster) {
  const url = `https://securitiesdb.com/api/v1/stocks/${cluster.ticker}/quant-health`;
  const res = await fetch(url);
  if (!res.ok) return cluster;

  const { data } = await res.json();
  return {
    ...cluster,
    piotroski: data.scores.piotroski_f,
    altman_z: data.scores.altman_z,
    signal_strength: data.scores.piotroski_f >= 7 ? '🟢 Strong' : '🟡 Moderate',
  };
}

What You Built

  • An automated insider cluster buy scanner using free SEC data
  • Discord notifications when 3+ insiders buy within 14 days
  • Cross-referencing with Piotroski and Altman Z scores for signal strength
  • Runs 24/7 with a simple Node.js process

Total code: ~100 lines. Zero API cost. Services like Fintel charge $400/year for similar insider alerting.

Related API Endpoints