Build a Company and Competitor News Monitor with Currents Search API
Build a Python company news monitor with a JSON watchlist, explicit date windows, source-linked reports, deterministic fixtures, and local deduplication.
A company news monitor needs more than a recurring keyword search. It needs a bounded window, a watchlist you can review, stable deduplication, and a report that keeps every publisher URL and publication time attached.
This guide builds that workflow with Currents Search API. The result is a Python command that reads a JSON watchlist and writes a source-linked Markdown report, structured JSON, and local state for the next run.
Currents provides search results and article metadata. The example owns the schedule, watchlist, local state, deduplication, and report format. It is not a managed monitor, alert service, entity-resolution system, or truth-scoring service.
What we are building
The monitor will:
- Read company, competitor, and industry searches from JSON.
- Require an explicit start and end time.
- Query each watch with language and optional domain filters.
- Remove results outside the window.
- Deduplicate articles that match several watches.
- Exclude article keys stored by earlier runs.
- Write source-linked Markdown and JSON reports.
The complete tested example is in
examples/company_news_monitor.
Start with a small watchlist
Create watchlist.json:
{
"watches": [
{
"name": "Battery competitors",
"query": "\"Northstar Battery\" OR \"Atlas Storage\"",
"language": "en"
},
{
"name": "Industry policy",
"keywords": "\"grid storage\" regulation",
"language": "en",
"domain": "example.com"
}
]
}
The company names are fictional. The first watch uses query for Boolean
syntax. The second uses keywords for standard search and adds a domain filter.
Each watch must define exactly one of these search fields.
Start with terms that describe a real decision. Add product names, executive names, locations, or industry terms when a company name is ambiguous. Search does not resolve entities or know which similarly named organization you meant.
Require a bounded window
A recurring monitor should never depend on an implicit time range. Parse both timestamps, require a timezone, and reject an inverted window:
from datetime import datetime, timezone
def parse_timestamp(value, label):
if not isinstance(value, str) or not value:
raise ValueError(f"{label} must be a non-empty RFC 3339 timestamp")
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise ValueError(f"{label} must include a timezone")
return parsed.astimezone(timezone.utc)
start = parse_timestamp("2026-08-04T09:00:00Z", "start_date")
end = parse_timestamp("2026-08-05T09:00:00Z", "end_date")
if start > end:
raise ValueError("start_date must not be after end_date")
The example treats both boundaries as inclusive. It also applies the window to fixture results, so offline tests exercise the same date behavior.
Call Search API for each watch
Live mode sends the documented query, date, language, and pagination parameters.
It adds domain when the watch defines one:
import os
import requests
SEARCH_URL = "https://api.currentsapi.services/v1/search"
def search_watch(watch, start_date, end_date, page_size=20):
search_fields = [
field for field in ("keywords", "query") if watch.get(field) is not None
]
if len(search_fields) != 1:
raise ValueError("watch requires exactly one of keywords or query")
search_field = search_fields[0]
params = {
search_field: watch[search_field],
"language": watch["language"],
"start_date": start_date,
"end_date": end_date,
"page_number": 1,
"page_size": page_size,
}
if watch.get("domain"):
params["domain"] = watch["domain"]
response = requests.get(
SEARCH_URL,
headers={
"Authorization": f"Bearer {os.environ['CURRENTS_API_KEY']}"
},
params=params,
timeout=20,
)
response.raise_for_status()
return response.json()
Keep the page size and date span within your plan. Available lookback, maximum search span, page size, and retrievable result count can vary by plan.
This example requests one page for each watch. If the API reports more matching results than your allowed page can return, the report is a bounded sample rather than an exhaustive archive.
The report accepts publisher-controlled metadata. The tested implementation requires HTTP or HTTPS source URLs, removes embedded line breaks, and escapes Markdown control characters before writing titles and descriptions.
Deduplicate before writing the report
One article can match a company name and an industry query. Report it once and retain every matching watch.
Treat the publisher URL and article ID as aliases. A match on either alias should merge the result:
def article_keys(article):
keys = [f"url:{article['url']}"]
if article.get("id"):
keys.append(f"id:{article['id']}")
return keys
The URL alias handles responses where an ID is missing. The ID alias handles URL variants for the same article.
The example stores both keys in company-news-monitor-state.json. On the first
run, every in-window result is new. Later runs exclude any article whose URL or
ID is already in that file.
Keep a separate state file for each monitor. If missing alerts would matter, your application should add locking, backups, recovery, and observability before scheduling the command.
Run the deterministic fixture
From a checkout of the Python repository, run:
python examples/company_news_monitor/monitor.py \
--fixture examples/company_news_monitor/fixtures/search_responses.json \
--state-file company-monitor-state.json \
--output-dir company-monitor-output
The fixture uses fictional companies and example.com source URLs. It contains
no credentials, customer data, or publisher article bodies.
The first run produces this report:
# Company News Change Report
Generated at: 2026-08-05T09:00:00Z
Window: 2026-08-04T09:00:00Z to 2026-08-05T09:00:00Z
- [Regulator publishes a grid storage consultation](https://example.com/policy/grid-storage-consultation)
- Published: 2026-08-05T08:00:00Z
- Watches: Industry policy
- A fictional regulator opens a consultation period.
- [Northstar Battery opens a pilot recycling line](https://example.com/business/northstar-pilot-line)
- Published: 2026-08-05T07:30:00Z
- Watches: Battery competitors
- A fictional company begins pilot operations.
- [Atlas Storage joins a grid demonstration](https://example.com/energy/atlas-grid-project)
- Published: 2026-08-04T14:00:00Z
- Watches: Battery competitors, Industry policy
- A fictional storage supplier joins a demonstration.
The JSON report contains the same URLs, publication times, matching watches, and
window. Run the command again with the same state file. The next report says
No new matching articles.
Run a live monitor
Set CURRENTS_API_KEY, then pass an explicit UTC window:
python examples/company_news_monitor/monitor.py \
--watchlist examples/company_news_monitor/watchlist.json \
--start-date 2026-08-04T09:00:00Z \
--end-date 2026-08-05T09:00:00Z \
--page-size 20 \
--state-file company-monitor-state.json \
--output-dir company-monitor-output
Use the previous successful end time as the next start time if you need continuous windows.
Decide how your application handles the shared boundary. The local article aliases make an inclusive shared boundary safe for sequential runs.
What this monitor does not solve
- Results reflect the Currents index and are not exhaustive.
- Keyword searches can miss aliases or match unrelated organizations.
- Domain filtering narrows publishers, not the real-world location of an event.
- Currents does not verify article claims or decide which report is true.
- Descriptions can be missing or truncated. Follow the publisher URL for context.
- The script does not send email, Slack, or webhook alerts.
- The script does not run on a schedule.
- API access does not grant publisher-content republication rights.
- The example does not provide production state recovery or concurrent-run locking.
- Markdown escaping reduces report injection risk. Applications that render JSON fields elsewhere must apply the output format's own validation and escaping.
Treat the output as a source-linked change report for your own workflow. Add human review when a report can trigger a customer, financial, legal, or operational decision.
To run the live example with your own watchlist, create a Currents API key.