The first version of my filings alert bot fired forty messages in an afternoon and I muted it by dinner. Every one of them was technically a new disclosure. Most were amendments to filings I had already seen, or the same congressional transaction reported twice because the clerk reposted the PDF with a different timestamp. That is the whole problem in miniature. Getting the raw feed is trivial. Turning it into something you actually read is where all the work lives, and almost none of it is glamorous.
So this is the build I settled on after a few rounds of that. Nothing exotic, just a poller, a deduplicator, and a delivery step, with the interesting decisions concentrated in the middle. If you have ever wanted alerts on Form 4 insider buys or congressional PTR filings landing in a chat channel, this is roughly the shape of it.
Where the data actually comes from
There are three main sources and they behave nothing alike. The SEC gives you the cleanest access. EDGAR has an RSS endpoint for recent filings that you can filter by form type, and there is a full-text search API on top of the same corpus if you want to query historically. For insider activity you care about Form 4 and Form 5, plus the occasional Form 3 for a new insider. The RSS feed updates promptly after filings post, so a poll on the order of a minute keeps you close to real time without hammering anything. One rule they enforce and mean it: set a descriptive User-Agent header with a contact address on every request. Requests without one get throttled or blocked, and it is the first thing that breaks silently when you copy someone else's script.
Congressional disclosures are messier because they are two systems, not one. The House clerk publishes Periodic Transaction Reports as a directory of PDFs plus an annual index file. The Senate runs a separate electronic filing portal with its own listing. Neither offers anything as convenient as EDGAR's RSS, so you end up polling an index and diffing it against what you saw last time. The cadence here can be lazy. These filings arrive in bursts tied to disclosure deadlines rather than continuously, so polling every fifteen to thirty minutes is plenty. Polling faster just wastes requests on a page that rarely changes.
A detail worth internalizing early: congressional PTRs are disclosed with a lag measured in weeks, not minutes. The transaction already happened. You are racing other readers of the same public document, not the trade itself. That reframes what low latency even means here. Shaving your pipeline from ten minutes to thirty seconds matters against other bots, and does nothing against the underlying delay.
Deduplication, which is most of the job
Every one of these sources will hand you the same logical event more than once, and each does it differently. If you skip this step your channel becomes unreadable, so I treat it as the core of the system rather than a cleanup pass.
The pattern that works is to compute a stable identity for each filing and keep a rolling record of what you have already sent. For EDGAR, the accession number is a genuinely unique key, so dedup there is close to free. Amendments are the trap. A Form 4/A is a corrected version of an earlier Form 4, and it arrives as its own filing with its own accession number, so naive keying treats it as brand new. You have to decide the policy on purpose. I send amendments, but I mark them clearly as corrections and I thread them to the original so the reader sees the relationship instead of a mysterious near-duplicate.
Congressional filings need a synthetic key because there is no clean accession number. I build one from the filer, the document ID or PDF filename, and the filing date, then hash it. Same document reposted, same hash, no second alert. Here is the checklist I actually run each item through before anything goes out:
- Compute a stable ID: accession number for EDGAR, a hash of filer plus document ID plus date for congressional feeds.
- Check it against a seen-store with time-to-live expiry, so the store does not grow forever but still covers the window where reposts happen.
- Flag amendments explicitly rather than dropping or blindly resending them, and link back to the original filing.
- Collapse multiple line items within one filing into a single message instead of one alert per transaction line.
That last point matters more than it looks. A single PTR can list a dozen trades. One message summarizing the filing beats twelve fragments that scroll your channel past the point of usefulness.
Filtering so the signal survives
Raw insider filings are mostly noise if your goal is to notice meaningful activity. A huge share of Form 4 events are routine: option grants, scheduled sales under a preset plan, tax withholding on vesting. None of those tell you much about conviction. The filings carry transaction codes, and learning a handful of them lets you cut volume hard. Open-market purchases, coded P, are the ones people usually care about, because an insider spending their own cash to buy is a different signal from a grant they were handed. Sales under a preset trading plan are worth de-emphasizing since they are scheduled in advance and say little about timing.
My rule of thumb is to default to showing purchases and large discretionary sales, and to route the routine grants and plan sales to a quieter channel or a daily digest rather than a live alert. You can always widen the filter later. Starting narrow and loosening beats starting wide and getting muted, which is the same lesson my forty-message afternoon taught me the expensive way.
Delivery to Telegram or Discord
The delivery step is the easy part, which is a nice change. Both platforms take an incoming webhook: you POST a JSON payload to a URL and a message appears in the channel. Discord webhooks accept rich embeds, so you can lay out ticker, insider name, transaction type, amount, and a link to the source filing in a structured card. Telegram's bot API takes formatted text and is just as quick to wire up.
A few things that save you grief once real volume hits. Respect rate limits by queuing outbound messages rather than firing them the instant you parse a filing, because a burst of filings becomes a burst of POSTs and you will get throttled at exactly the wrong moment. Make the send idempotent against your seen-store, so a crash and restart mid-batch does not replay everything you already delivered. And always include a direct link to the primary source in every alert. The point of the pipeline is to get you to the real document faster, not to replace reading it, and half the value of an alert is being one click from the filing itself.
Put together, the whole thing is a loop that polls three feeds on their own cadences, keys and dedups every item, filters down to what you would actually want interrupted for, and posts the survivors to a webhook. The hard-won part is not any single piece. It is the discipline to make dedup and filtering first-class instead of afterthoughts, because that is the difference between a tool you keep and one you mute by dinner.