NestDaddy
News API

Build a daily news digest bot in Python (Telegram, Slack or email)

A step by step guide to a small Python bot that collects world, tech and markets headlines plus your own keyword alerts every morning, removes duplicates and sends one clean digest to Telegram, Slack or email. Full code included.

NestDaddy team · · 7 min read

Most people follow news the hard way: ten tabs, three apps and a feed that never ends. A daily digest fixes that. One message every morning with the headlines that matter to you and alerts for the topics you track, and nothing else.

In this guide you will build that digest bot in about 150 lines of Python. It will:

  1. Pull the latest world, tech and markets headlines.
  2. Search the last 24 hours of news for your own keywords (a competitor, a product, a technology).
  3. Remove anything it already sent you before.
  4. Send one tidy message to Telegram, Slack or email.
  5. Run on a schedule with cron or GitHub Actions, for free.

Want to skip the copy and paste? The finished bot is a GitHub template: nestdaddy/news-digest-bot. Click Use this template, add your secrets and it runs every morning.

The news comes from the NestDaddy API, which collects news from 45+ countries and returns plain JSON. You only need Python 3.9+ and the requests library.

What you need

  • A NestDaddy API key. The free 7-day trial gives you 5,000 calls, and this bot uses about 6 calls a day. Sign up on the MCP and plans page.
  • Somewhere to send the digest: a Telegram bot, a Slack incoming webhook, or an SMTP account.
  • pip install requests

Put your secrets in environment variables, never in the code:

export NESTDADDY_KEY="your_key"
export TELEGRAM_TOKEN="123456:ABC..."   # optional
export TELEGRAM_CHAT_ID="123456789"     # optional
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."  # optional

Step 1: fetch the headlines

NestDaddy has one endpoint per news section. Each returns an articles list with title, url, description, source_domain and published_timestamp:

Section Endpoint
World GET /api/v1/news/global
Tech GET /api/v1/news/tech
Markets, crypto and economy GET /api/v1/news/financial
import os
import requests

API = "https://nestdaddy.com/api/v1"
HEADERS = {"X-API-Key": os.environ["NESTDADDY_KEY"]}


def get_json(path, **params):
    r = requests.get(f"{API}{path}", headers=HEADERS, params=params, timeout=30)
    r.raise_for_status()
    return r.json()


def headlines(section, limit=5):
    data = get_json(f"/news/{section}", limit=limit)
    return data.get("articles", [])

limit goes up to 50. Five per section keeps the digest short enough to read over coffee.

Step 2: add your own keyword alerts

Headlines tell you what everyone is talking about. Keyword alerts tell you what you care about. The news search endpoint takes key words, an optional two-letter country code and how many days back to look:

WATCHLIST = [
    {"q": "OpenAI"},
    {"q": "electric vehicles", "country": "US"},
    {"q": "interest rates", "country": "GB"},
]


def keyword_alerts(watch, limit=5):
    params = {"q": watch["q"], "days": 1, "limit": limit}
    if watch.get("country"):
        params["country"] = watch["country"]
    data = get_json("/news/search", **params)
    return data.get("results", [])

days=1 keeps only the last 24 hours. Leave country out to search every country at once, or set it to focus on one market (for example US, GB or SG).

Step 3: never send the same story twice

The same article can show up in the world section and in a keyword alert, and yesterday's story can still be near the top today. A tiny SQLite table of URLs you already sent solves both:

import sqlite3
from urllib.parse import urlsplit, urlunsplit

db = sqlite3.connect("digest_seen.db")
db.execute("CREATE TABLE IF NOT EXISTS seen (url TEXT PRIMARY KEY, sent_at TEXT)")


def clean_url(url):
    # Drop query strings and fragments so tracking tags do not create duplicates
    p = urlsplit(url)
    return urlunsplit((p.scheme, p.netloc.lower(), p.path.rstrip("/"), "", ""))


def only_new(articles):
    fresh = []
    for a in articles:
        key = clean_url(a["url"])
        if db.execute("SELECT 1 FROM seen WHERE url = ?", (key,)).fetchone():
            continue
        db.execute("INSERT INTO seen VALUES (?, datetime('now'))", (key,))
        fresh.append(a)
    return fresh

Call db.commit() only after the message was sent, so a failed send does not mark stories as seen.

Step 4: build the message

Keep it plain: a section title, then one line per story with the source. Telegram and Slack both handle simple Markdown-style links, so we build one text body and adapt it per channel:

def build_digest():
    sections = []
    for title, section in [("World", "global"), ("Tech", "tech"), ("Markets", "financial")]:
        items = only_new(headlines(section))
        if items:
            sections.append((title, items))
    for watch in WATCHLIST:
        items = only_new(keyword_alerts(watch))
        if items:
            label = watch["q"] + (f" ({watch['country']})" if watch.get("country") else "")
            sections.append((f"Alert: {label}", items))
    return sections


def as_text(sections):
    lines = []
    for title, items in sections:
        lines.append(f"\n{title.upper()}")
        for a in items:
            lines.append(f"- {a['title']} ({a.get('source_domain', '')})\n  {a['url']}")
    return "\n".join(lines).strip()

Step 5: send it

Telegram

Create a bot with @BotFather, send it one message, then open https://api.telegram.org/bot<TOKEN>/getUpdates to find your chat id. Telegram messages are limited to 4,096 characters, so long digests are split:

def send_telegram(text):
    token, chat = os.environ["TELEGRAM_TOKEN"], os.environ["TELEGRAM_CHAT_ID"]
    for i in range(0, len(text), 4000):
        r = requests.post(
            f"https://api.telegram.org/bot{token}/sendMessage",
            json={"chat_id": chat, "text": text[i:i + 4000],
                  "disable_web_page_preview": True},
            timeout=30,
        )
        r.raise_for_status()

Slack

Create an incoming webhook for your channel in your Slack app settings, then:

def send_slack(text):
    r = requests.post(os.environ["SLACK_WEBHOOK_URL"], json={"text": text}, timeout=30)
    r.raise_for_status()

Email

import smtplib
from email.message import EmailMessage


def send_email(text):
    msg = EmailMessage()
    msg["Subject"] = "Your daily news digest"
    msg["From"] = os.environ["SMTP_FROM"]
    msg["To"] = os.environ["SMTP_TO"]
    msg.set_content(text)
    with smtplib.SMTP_SSL(os.environ["SMTP_HOST"], 465) as s:
        s.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
        s.send_message(msg)

Step 6: put it together

import sys


def main():
    sections = build_digest()
    if not sections:
        print("Nothing new today")
        return
    text = as_text(sections)
    if os.environ.get("TELEGRAM_TOKEN"):
        send_telegram(text)
    if os.environ.get("SLACK_WEBHOOK_URL"):
        send_slack(text)
    if os.environ.get("SMTP_HOST"):
        send_email(text)
    db.commit()  # mark stories as sent only after delivery worked
    print(f"Sent {sum(len(i) for _, i in sections)} stories")


if __name__ == "__main__":
    try:
        main()
    except requests.HTTPError as e:
        print("Request failed:", e.response.status_code, e.response.text[:200], file=sys.stderr)
        sys.exit(1)

Run it once with python digest.py and check your phone.

Step 7: run it every morning

With cron (any Linux server or Mac)

# 07:00 every day
0 7 * * * cd /home/you/digest && /usr/bin/python3 digest.py >> digest.log 2>&1

With GitHub Actions (no server at all)

Add your keys as repository secrets, then create .github/workflows/digest.yml:

name: daily-digest
on:
  schedule:
    - cron: "0 7 * * *"   # 07:00 UTC
  workflow_dispatch:
jobs:
  send:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install requests
      - run: python digest.py
        env:
          NESTDADDY_KEY: ${{ secrets.NESTDADDY_KEY }}
          TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}

GitHub Actions starts with a clean machine each run, so the SQLite file is lost between runs. Either commit digest_seen.db back to the repo at the end of the job, or skip the database and use days=1 alone, which already keeps most repeats out.

Ideas to take it further

How many calls does it use?

Three headline calls plus one call per watchlist entry. With three keywords that is 6 calls a day, about 180 a month. The free trial covers that many times over, and every response includes a quota object (used, limit, remaining) so the bot can warn you before you run low.

Full API reference: nestdaddy.com/apis. More examples in Python, JavaScript and curl: nestdaddy/nestdaddy-examples.

More guides