← All posts

How to Build a Discord Bot That Searches and Summarizes the Live Web

A Discord bot that can answer "what is happening with X right now" is one of the most useful things you can drop into a server. Support channels, gaming communities, and startup Slack-replacement servers all end up asking questions that a plain language model cannot answer, because the model stopped reading the day its training data was frozen. Bolt live web access onto the bot and it stops guessing.

This post builds a /ask slash command that searches the web, reads the top results, and posts a short summary with sources back into the channel. The web layer is link.sc, the bot framework is discord.py, and the whole thing is about 60 lines.

What the bot actually needs to do

The flow is three steps. A user types /ask what changed in the latest Python release. The bot searches the web for that query, pulls the content of the best few results, and hands that content to a language model with an instruction to summarize and cite. The model writes the answer from what it just read rather than from memory, and the bot posts it.

The hard part is not the Discord wiring. Slash commands are well documented and the framework handles the plumbing. The hard part is reliably reading arbitrary web pages: many render with JavaScript and return an empty shell to a plain HTTP request, some sit behind Cloudflare and block datacenter IPs, and every HTML-to-text pipeline breaks on a new site eventually. That is the piece we offload.

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 one call gives you both the links and the readable content. /v1/fetch takes a single URL and returns clean markdown, JSON, HTML, or a screenshot. It handles rendering, proxies, and retries internally by escalating cheapest-first, so your bot code never decides whether a site needs a browser.

For an /ask command, search alone usually does the job, because it returns page content inline. You reach for fetch when a user pastes a specific URL and wants it summarized.

Registering the slash command

Start with the discord.py skeleton. You need a Client, a CommandTree to hold slash commands, and one intent set. The command is declared with a decorator, and the descriptions you write become the text Discord shows users in the command picker.

import os
import discord
from discord import app_commands

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)


@client.event
async def on_ready():
    await tree.sync()
    print(f"logged in as {client.user}")


@tree.command(name="ask", description="Search the live web and summarize the answer")
@app_commands.describe(query="What do you want to know?")
async def ask(interaction: discord.Interaction, query: str):
    await interaction.response.defer(thinking=True)
    answer = await answer_question(query)
    await interaction.followup.send(answer)

The one detail that trips people up: interaction.response.defer(thinking=True). A slash command has to be acknowledged within three seconds, and searching plus summarizing takes longer than that. Deferring tells Discord to show a "thinking" state and buys you up to fifteen minutes to reply with followup.send.

Searching and reading the web

Now the function that does the work. It calls the search endpoint, keeps the top three results, and stitches their content into one block of text for the model to read.

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 matters for the citation step below.

Summarizing with a grounded prompt

Tool access alone does not stop a model from making things up. The instruction does. Tell the model to answer only from the text you handed it, to admit when the text does not contain the answer, and to cite the source number for each claim. That shifts its job from recall to reading comprehension, which is what language models are genuinely 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

Keeping the answer under 150 words is not only about cost. Discord messages cap at 2000 characters, and a tight summary reads better in a chat channel than a wall of text. If you want longer answers, split them across follow-up messages or post an embed.

Running it

Set two environment variables, your Discord bot token and your link.sc key, and start the client.

client.run(os.environ["DISCORD_TOKEN"])

Invite the bot to a server with the applications.commands scope enabled, wait for tree.sync to register the command, and /ask shows up in the slash-command menu. Type a question and the bot defers, searches, summarizes, and posts the grounded answer with source numbers a few seconds 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 is plenty to build and test a real bot before you pay anything.

For a busy server, add a per-user cooldown so one person cannot fire fifty searches a minute. discord.py has app_commands.checks.cooldown for exactly this, and it costs you one decorator.

Where to take it next

Swapping the model is a one-line change, since the identifier is just a string. You can add a second /summarize command that takes a URL and calls /v1/fetch directly, which is useful when someone drops a link and wants the gist without reading it. 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.

The upshot: a Discord bot with live web access is a slash command, a search call, and a grounded prompt. Discord handles the chat surface, the language model handles the writing, and link.sc handles the messy part of actually reading the web.


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