← All posts

How to Build a Telegram Bot That Searches and Reads the Live Web

The quick version: a Telegram bot answers a message by running the text through a web search, reading the top results, and handing that content to a language model that writes a grounded reply with sources. Telegram delivers each message to your server as a webhook POST, so the whole bot is one HTTP handler. The web layer is link.sc, the framework is FastAPI, and the finished bot is about 70 lines.

Telegram is a natural home for this. People already run group chats for teams, communities, and side projects there, and the moment someone asks "what is the latest on X," a plain language model is stuck: it stopped reading the day its training data was frozen. Give the bot live web access and it stops guessing.

Webhook, not polling

There are two ways a Telegram bot receives messages. Long polling has your code call getUpdates in a loop and wait for Telegram to hand back new messages. Webhooks flip that: you register a public HTTPS URL once, and Telegram POSTs each update to it as it arrives. Webhooks are the better fit for anything that runs on a server, because there is no idle loop, no missed-message window when your process restarts, and the request/response cycle maps cleanly onto a single endpoint.

This is the main thing that makes a Telegram bot feel different from a Slack or Discord build. There is no slash-command registry to sync and no gateway socket to keep alive. You own one route, Telegram calls it, you reply. That is the whole contract.

The web layer

link.sc turns web access into two endpoints. /v1/search takes a query and returns ranked results with full-page markdown already extracted, so a single call gives you both the links and the readable content. /v1/fetch takes one URL and returns clean markdown, JSON, HTML, or a screenshot. It handles rendering, proxies, and retries internally by escalating cheapest-first, so your bot never has to decide whether a page needs a real browser.

That last part is the piece worth offloading. Reading arbitrary web pages is genuinely hard: many render with JavaScript and return an empty shell to a plain HTTP request, some sit behind Cloudflare and block datacenter IPs, and every homegrown HTML-to-text pipeline breaks on a new site eventually. For a bot that answers questions, search alone usually does the job, because it returns page content inline. You reach for fetch when someone pastes a specific link and wants it read.

Searching and reading the web

Start with the function that does the real work, so the webhook handler stays thin. It calls the search endpoint, keeps the top three results, and stitches their content into one block for the model to read.

import os
import httpx

LINKSC = "https://api.link.sc/v1"
HEADERS = {"x-api-key": os.environ["LINKSC_KEY"]}


async def web_search(query: str) -> str:
    async with httpx.AsyncClient(timeout=60) as http:
        r = await http.post(
            f"{LINKSC}/search",
            headers=HEADERS,
            json={"q": query},
        )
        r.raise_for_status()
        results = r.json()["results"][:3]
    return "\n\n".join(
        f"[{i + 1}] {hit['title']} ({hit['url']})\n{hit['content']}"
        for i, hit in enumerate(results)
    )

Capping at three results keeps token count and cost down while still giving the model enough to work with. Each block carries a numbered label, the title, and the URL, which the citation step below depends on.

Grounding the answer

Web access alone does not stop a model from inventing things. The instruction does. Tell the model to answer only from the text you handed it, to admit when that text does not contain the answer, and to cite the source number for each claim. That turns the task from recall into reading comprehension, which is what language models are actually good at.

import anthropic

llm = anthropic.AsyncAnthropic()


async def answer_question(query: str) -> str:
    context = await web_search(query)
    resp = await llm.messages.create(
        model="claude-opus-4-8",
        max_tokens=600,
        system=(
            "Answer the question using only the numbered search results. "
            "If they do not contain the answer, say so. Keep it under 150 "
            "words and cite the result number for each claim, like [1]."
        ),
        messages=[{
            "role": "user",
            "content": f"Question: {query}\n\nSearch results:\n{context}",
        }],
    )
    return resp.content[0].text

The webhook handler

Now the part that makes it a Telegram bot. FastAPI receives the update, pulls the chat id and message text out of the JSON payload, runs the pipeline, and posts the reply back through Telegram's sendMessage method.

from fastapi import FastAPI, Request

app = FastAPI()
TOKEN = os.environ["TELEGRAM_TOKEN"]
TG = f"https://api.telegram.org/bot{TOKEN}"


@app.post("/webhook")
async def webhook(request: Request):
    update = await request.json()
    message = update.get("message")
    if not message or "text" not in message:
        return {"ok": True}

    chat_id = message["chat"]["id"]
    answer = await answer_question(message["text"])

    async with httpx.AsyncClient(timeout=30) as http:
        await http.post(
            f"{TG}/sendMessage",
            json={"chat_id": chat_id, "text": answer},
        )
    return {"ok": True}

Two details matter here. First, always return {"ok": True} quickly, even for updates you ignore, because Telegram retries any webhook call that does not get a 200 and you do not want it re-delivering edits, joins, and stickers your handler cannot use. Second, guard for messages that have no text field, since photos and system events arrive on the same endpoint.

Registering the webhook

Telegram needs to know where to send updates. Point it at your public HTTPS URL once with a single API call. During local development, a tunnel such as ngrok gives you a public address that forwards to your machine.

curl "https://api.telegram.org/bot$TELEGRAM_TOKEN/setWebhook?url=https://your-app.example.com/webhook"

Set three environment variables before starting the server: your Telegram token from BotFather, your link.sc key, and your LLM key. Then run FastAPI with uvicorn app:app, and every message sent to the bot lands on /webhook a moment later.

Cost and rate limits

Two meters matter. LLM tokens are the first, and returning markdown instead of raw HTML plus capping at three results keeps them low. Web-data calls are the second: on link.sc, one search or one page is roughly one credit, and there are 500 free credits a month, which covers building and testing a real bot before you pay anything.

For a busy group chat, add a per-user cooldown so one person cannot fire fifty searches a minute, and consider only responding when the bot is mentioned or messaged directly rather than to every line in the channel. Telegram also rate-limits sendMessage, so keep replies to one message rather than splitting a long answer across many.

Where to take it next

The obvious extension is a /read command: when someone pastes a URL, call /v1/fetch on it directly and summarize the page without a search step. Swapping the model is a one-line change, since the identifier is just a string. The same search-read-answer pattern powers agents in any framework, and the general version is walked through in giving an AI agent internet access. If you want the pipeline broken down on its own, see real-time web search for LLMs, and if you would rather build on Discord, there is a companion guide for that.

The upshot: a Telegram bot with live web access is one webhook, one search call, and a grounded prompt. Telegram handles delivery, the language model handles the writing, and link.sc handles the messy part of actually reading the web.


Give your Telegram bot live web access today. Grab a key at link.sc/register and start with 500 free credits a month.