Monitoring OpenAI news in Python, where entity filters fail

We wanted to know when OpenAI ships a model, loses an executive, or gets sued, without refreshing anyone's timeline. The script that does this is about eighty lines. Getting to those eighty lines took a week of measurement, and most of what we measured contradicted the advice we started with.
A news watcher is a scheduled query against a news index plus the filtering you write yourself: deduplication, theme routing, and a noise threshold. The query is the easy part. Every vendor blog stops there. This one doesn't, because the query returned 820 articles in a week and we only wanted about fifteen a day.
The watcher runs four steps in this order, and every one of them turned out to matter:
Query one day of coverage from a news index, paging until the index says there's no next page.
Deduplicate by title token overlap, because the same filing reaches you from a dozen newsrooms.
Route each article into a theme — launch, people, legal, outage, money — with regex over title and description.
Threshold on source authority, then deliver what's left.
All numbers below come from one collection run against the APITube news API: English-language articles with "OpenAI" in the title, 30 August to 6 September 2026, 820 articles across 372 sources. Disclosure: we work on APITube. Where it fell over, we say so — most of this article is about it falling over.
The obvious approach, and why it failed for OpenAI
Every guide to company monitoring says the same thing: don't search for a keyword, filter by the company entity. Keyword search catches "Apple" the fruit; entity resolution catches Apple Inc. It's good advice. It also assumes the entity exists in the vendor's graph.
APITube has an organization.name filter. Here is what a week of coverage looks like through it:
| Filter value | Articles, 30 Aug – 6 Sep | What came back |
|---|---|---|
organization.name=Microsoft |
4,123 | Microsoft coverage |
organization.name=Anthropic |
2,080 | Anthropic coverage |
organization.name=Nvidia |
1,770 | Nvidia coverage |
organization.name=Apple |
4,123 | includes Gooey Apple Cinnamon Rolls |
organization.name=Tesla |
11 | Tesla coverage, almost none of it |
organization.name=Meta |
0 | nothing |
organization.name=OpenAI |
— | entity organization name 'OpenAI' not found |
organization.name=Alphabet |
— | entity organization name 'Alphabet' not found |
title=OpenAI |
828 | OpenAI coverage |
OpenAI isn't in the entity graph at all. Neither is Alphabet. Meta resolves to something with zero articles, Tesla to something with eleven, and Apple returns the same count as Microsoft while serving us a cinnamon roll recipe. We also pulled every organization entity out of 195 OpenAI articles: Hugging Face, SES S.A., Microsoft, Anthropic, Gruppo API, and a person called Joseph Altman. Not one mention of OpenAI.
So the rule isn't "use entity filters" or "use keywords". It's check whether your entity exists before you design around it, which is one request:
curl -s "https://api.apitube.io/v1/companies?name=OpenAI" \
-H "X-API-Key: $APITUBE_API_KEY"
# {"status":"ok","limit":100,"page":1,"has_next_pages":false,"results":[]}
Empty array. The autocomplete endpoint agrees — GET /v1/suggest/entities?prefix=OpenAI returns Openair Frauenfeld, a Swiss music festival. Unlike a keyword search, an entity filter fails silently when the entity is missing: you get zero rows and no error telling you the concept doesn't exist, which means a watcher built on it stays quiet forever and looks like it's working.
For OpenAI, the watcher runs on title=OpenAI and does its own filtering downstream.
What the watcher queries
curl -s -G "https://api.apitube.io/v1/news/everything" \
-H "X-API-Key: $APITUBE_API_KEY" \
-d title=OpenAI \
-d language.code=en \
-d published_at.start=2026-09-05 \
-d published_at.end=2026-09-06 \
-d sort.by=published_at -d sort.order=desc \
-d per_page=100
One result, trimmed to the fields the watcher touches:
{
"id": 3079204730,
"title": "Seattle Times, Newsday Take OpenAI And Microsoft To Court Over Copyright Claims",
"href": "https://www.timesnownews.com/technology-science/seattle-times-newsday-take-openai-and-microsoft-to-court-over-copyright-claims-article-156045463",
"published_at": "2026-09-05T04:23:43.000Z",
"source": { "domain": "timesnownews.com", "rankings": { "opr": 6 } },
"sentiment": { "overall": { "polarity": "neutral" } },
"story": { "id": 3079204730 },
"is_duplicate": false
}
Two things bit us here, both worth knowing before you schedule anything.
published_at.end is exclusive. Asking for start=2026-09-06&end=2026-09-06 returns zero rows; end=2026-09-07 returns the day. A daily watcher written the obvious way skips every day it runs and never errors.
Unknown parameters are sometimes ignored in silence. entity.name, body and keyword at least come back flagged in meta.warnings, so log that array. search doesn't: search=zzqqxwv returned three cheerfully unrelated articles. Before trusting any filter, send it a nonsense value and check you get nothing back.
Deduplication is the part nobody writes about
820 articles from 372 sources over seven days, and the same story arrives four or five times. We tried four ways of collapsing them on the same dataset:
| Method | Duplicates caught | Share of 820 |
|---|---|---|
The API's is_duplicate flag |
0 | 0.0% |
Grouping by story.id |
8 | 1.0% |
| Exact match on normalised titles | 106 | 12.9% |
| Token-set overlap ≥ 0.6 | 179 | 21.8% |
The vendor's own duplicate flag was false on all 820 rows. Story clustering caught eight. Whatever your provider promises about deduplication, measure it on your own query before you rely on it — ours cost us one line of Python to check and would have cost us a fifth of our alert volume to skip.
Token overlap is what the watcher uses. Strip punctuation and stopwords, compare titles as sets, drop anything that overlaps an already-kept title by 60% or more:
STOP = set("a an the of in on at to for and or with by from is are was were as "
"its it this that new says said after over into be will has have "
"how why what when who".split())
def tokens(title):
return {w for w in re.sub(r"[^a-z0-9 ]", " ", title.lower()).split()
if w not in STOP and len(w) > 2}
def dedup(articles):
kept, seen = [], []
for a in articles:
t = tokens(a["title"])
if any(len(t & s) / len(t | s) >= 0.6 for s in seen if t and s):
continue
seen.append(t)
kept.append(a)
return kept
It catches syndication, not rewriting. On 5 September the copyright suit reached us as four separate alerts — "Seattle Times, Newsday sue OpenAI, Microsoft", "Seattle Times, Newsday Take OpenAI And Microsoft To Court", "US newspapers sue OpenAI, Microsoft", and "Newsday sues OpenAI and Microsoft" — because four newsrooms wrote four genuinely different sentences. Dropping the threshold to catch those starts merging unrelated stories. We kept the duplicates and moved on; embeddings would fix it, and would also turn eighty lines into a service.
Routing into launches, departures and lawsuits
Five regexes over title plus the first 400 characters of the description:
THEMES = {
"launch": r"(launch\w*|releas\w*|unveil\w*|introduc\w*|roll\w* out|debut\w*|announc\w*|general availability|preview)",
"people": r"(hir\w+|join\w+|depart\w+|resign\w*|step\w* down|quit\w*|fired|ousted|appoint\w*|new (ceo|cto|cfo|president))",
"legal": r"(lawsuit\w*|sue[sd]?\b|suing|court|judge|settl\w+|copyright|antitrust|subpoena|alleg\w+|litigation|injunction|regulat\w+|probe|investigat\w+)",
"outage": r"(outage\w*|downtime|degrad\w+|incident\w*|status page|disruption\w*|restored|went down)",
"money": r"(funding|fundrais\w*|valuation|\bround\b|invest\w+|acqui\w+|\bipo\b|billion (deal|agreement)|stake)",
}
THEMES = {k: re.compile(v, re.I) for k, v in THEMES.items()}
def route(a):
text = a["title"] + " " + (a.get("description") or "")[:400]
return [name for name, rx in THEMES.items() if rx.search(text)]
Across the week: launch 284 articles (34.6%), legal 191 (23.3%), money 68 (8.3%), outage 62 (7.6%), people 53 (6.5%). 259 articles (31.6%) matched nothing and 90 (11.0%) matched more than one theme.
Reading the description roughly doubles recall and imports someone else's summary into your routing. Of 62 outage matches, 38 matched only in the description — including "OpenAI Says It Wants to Create a Standard for Revealing AI Alignment Meltdowns", which is a policy story that says "incidents" in its second sentence. Match people and legal across title and description, because a departure is often buried in paragraph three. Match outage on the title alone.
That 31.6% residual is not a bug to tune away. It's GPT-6 benchmark scores, congressional candidates expensing subscriptions, and agents editing a German wiki — real coverage that no keyword set anticipated. Send it as a low-priority bucket rather than dropping it.
How loud should the watcher be
Every article carries source.rankings.opr, an authority score from 0 to 8. Pick your threshold by how many alerts a day you'll actually read:
| Threshold | Articles kept | Share | Per day |
|---|---|---|---|
opr >= 3 |
783 | 95.5% | 112 |
opr >= 4 |
696 | 84.9% | 99 |
opr >= 5 |
531 | 64.8% | 76 |
opr >= 6 |
246 | 30.0% | 35 |
opr >= 7 |
78 | 9.5% | 11 |
Below 5 you're subscribing to the whole internet — the top ten domains are only 17.2% of volume, so the long tail is where the count lives. At 7 you get eleven a day and you're reading TechCrunch, which you already do. We run at 6.
The table above applies the threshold to raw articles. In the watcher it runs after dedup, so the numbers differ: 820 raw → 641 after dedup → 177 above opr >= 6 → 118 with a theme. Seventeen alerts a day, of which the ones we'd have wanted to know about within the hour — the GPT-6 Astra launch, four newsrooms filing suit, the wiki incident — all survived.
Cost, since nobody mentions it: a day of OpenAI coverage is one to two requests at per_page=100, so a daily digest is about 500 requests a year and a fifteen-minute loop is roughly 35,000. Both fit inside a free tier on most providers. The expensive version is the one that polls every source separately.
The whole script
import os, re, sqlite3, datetime as dt
import requests
API = "https://api.apitube.io/v1/news/everything"
KEY = os.environ["APITUBE_API_KEY"]
MIN_OPR = 6
def fetch(day, query="OpenAI"):
"""One day of coverage. published_at.end is exclusive — pass the NEXT day."""
out, page = [], 1
while True:
r = requests.get(API, headers={"X-API-Key": KEY}, timeout=30, params={
"title": query,
"language.code": "en",
"published_at.start": day.isoformat(),
"published_at.end": (day + dt.timedelta(days=1)).isoformat(),
"sort.by": "published_at", "sort.order": "desc",
"per_page": 100, "page": page,
})
r.raise_for_status()
body = r.json()
for w in (body.get("meta") or {}).get("warnings", []):
print("API warning:", w["message"])
out += body.get("results", [])
if not body.get("has_next_pages"):
return out
page += 1
def loud_enough(a):
return (a["source"].get("rankings", {}).get("opr") or 0) >= MIN_OPR
def main():
db = sqlite3.connect("seen.db")
db.execute("CREATE TABLE IF NOT EXISTS seen (id INTEGER PRIMARY KEY)")
yesterday = dt.date.today() - dt.timedelta(days=1)
raw = fetch(yesterday)
fresh = [a for a in raw
if not db.execute("SELECT 1 FROM seen WHERE id=?", (a["id"],)).fetchone()]
digest = [a for a in dedup(fresh) if loud_enough(a) and route(a)]
for a in digest:
print(f"[{'/'.join(route(a))}] {a['title']} "
f"({a['source']['domain']}, opr {a['source']['rankings']['opr']})")
db.executemany("INSERT OR IGNORE INTO seen VALUES (?)", [(a["id"],) for a in raw])
db.commit()
if __name__ == "__main__":
main()
The SQLite table exists because reprints trickle in for days and the dedup pass only sees one run at a time. Point print at a Telegram sendMessage call or an SMTP digest and you're done. Run it on cron at 08:00 for a morning digest, or every fifteen minutes if you want legal and outage fast — the freshest article in our collection was 53 minutes old when we pulled it, so a quarter-hour loop is not obviously wasteful.
Real output, 5 September:
2026-09-05: 87 articles -> 78 after dedup -> 15 alerts
[outage] OpenAI confirms 'wiki incident,' says it's 'working on a framework' (techcrunch.com, opr 8)
[launch] OpenAI Launches GPT-6 Astra With Computer Use Tools (dev.to, opr 7)
[legal] Newsday sues OpenAI and Microsoft over copyright infringement (newsday.com, opr 6)
[launch/money] Nvidia inks $13 billion deal to buy the AI startup hacked by OpenAI (egyptindependent.com, opr 6)
Swap the query, keep the pipeline
Change title=OpenAI to organization.name=Anthropic and everything downstream still works — dedup, routing and thresholds don't care where the rows came from. That's the point of doing the filtering yourself. Just re-run the entity check first, and re-measure the duplicate rate: 21.8% is what OpenAI's coverage looked like, not a constant.
Questions we had while building this
How do I monitor OpenAI news automatically?
Monitoring OpenAI news automatically means scheduling a daily query against a news index for articles with OpenAI in the title, then deduplicating, routing into themes, and thresholding on source authority before anything is delivered. The query takes ten lines; the filtering is where the work is. On our dataset that pipeline reduced 820 articles a week to 17 alerts a day.
Does OpenAI have an RSS feed?
OpenAI publishes an RSS feed at https://openai.com/news/rss.xml, free and complete for anything the company says about itself. It will never carry a lawsuit filed against OpenAI, an executive leaving, or an outage the company hasn't acknowledged yet. Subscribe to it and run a third-party watcher; they cover different things.
How do I track a company's news with Python?
Tracking a company's news in Python starts with checking that the entity exists (/v1/companies?name=...), falling back to title search if it doesn't, paging through with per_page=100, remembering what you've already sent in SQLite, and deduplicating by token overlap. That's roughly eighty lines with requests and the standard library.
How do you filter duplicate news articles?
Duplicate news articles are best filtered by comparing normalised titles as token sets and dropping anything that overlaps an already-kept title by 60% or more. On our dataset that caught 179 of 820 articles, against 106 for exact title matching and 0 for the API's own duplicate flag. Rewritten headlines still slip through; embeddings are the next step.
What's the best API for AI news?
The best API for AI news is whichever one resolves the entities you actually care about, and that is a single request to find out — don't take a vendor's entity coverage on faith, including ours. We used APITube because it exposes source authority scores and a count endpoint, which is what made the thresholds in this article measurable. APITube is one of the APIs we mentioned — there's a free tier at apitube.io.
Resources
APITube news API docs — endpoints and filters used here
OpenAI news RSS feed — first-party announcements
perigon.io on monitoring OpenAI — the entity-first playbook this article argues with

