← All posts

How to Do Real-Time Web Search for Your LLM App

real time web search for llms

Every LLM has a training cutoff. Ask it about a release from last week, a price that changed yesterday, or a score from last night, and it either refuses or — worse — confidently makes something up. If you're building anything that needs to answer questions about the current state of the world, you need to get live data into the model's context at query time. That means web search.

This sounds simple and mostly isn't. The naive version — call a search API, dump the results into the prompt — produces shallow, sometimes wrong answers, because most search APIs give you snippets, not content. Let's go through why LLMs need live search, where the common approach breaks down, and how to build a flow that actually grounds answers in real page content.

Why LLMs Need Live Search

Two problems, really. The first is staleness: the model's knowledge stops at its training cutoff, so anything after that is a blind spot. The second is specificity: even for events before the cutoff, the model has a compressed, lossy memory of the web. It might remember that a library exists but not its current API, or know a company but not this quarter's numbers.

Retrieval fixes both. Instead of asking the model to recall facts, you fetch the relevant facts at runtime and let the model reason over them. The model's job shifts from "remember everything" to "read these sources and answer" — which is what LLMs are actually good at, and which lets you cite where each claim came from.

Snippets vs. Full Page Content

Here's the trap. A traditional search API returns something like this per result: a title, a URL, and a one- or two-sentence snippet. That snippet is optimized for a human scanning a results page, not for answering a question.

If you feed only snippets to your LLM, you're asking it to answer from blurbs. For "what's the capital of France," fine. For "what changed in the 3.0 release" or "how do I configure this feature," the snippet almost never contains the answer — the answer is three paragraphs down on the actual page. The model then does one of two things: hedges, or hallucinates a plausible-sounding answer that isn't grounded in anything.

So the real flow has an extra step. You search to find candidate URLs, then you fetch each page to get its full content, then you feed that content to the model. Search → read → answer:

import requests

# 1. Search for candidate sources
hits = search("what changed in the 3.0 release")

# 2. Read the full content of the top results
docs = [fetch(hit["url"]) for hit in hits[:3]]

# 3. Answer, grounded in the fetched content
answer = llm(prompt="Answer using these sources:\n" + "\n\n".join(docs))

That middle step — reading full pages — is where most of the engineering pain lives. You have to fetch pages that fight back (bot walls, JS rendering), strip nav and ads and cookie banners, and convert messy HTML into clean text the model can actually use. Doing that reliably across arbitrary sites is a project in itself.

Building the Flow Without the Fetch Tax

The nice thing is you can collapse search-and-read into a single call. link.sc's /search endpoint returns real-time results with the full page content already extracted as markdown — so you skip the second round of fetch requests entirely. One call gives you ranked results and the clean, model-ready text for each.

curl -X POST https://api.link.sc/v1/search \
  -H "x-api-key: lsc_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "what changed in the 3.0 release"}'

Each result comes back with its title, URL, and full-page markdown. That markdown is what you drop straight into your prompt — no HTML parsing, no per-page fetch loop, no bot-wall handling on your side (under the hood link.sc escalates from plain HTTP to a stealth browser only when a site requires it, and remembers what each domain needs).

Here's the whole search-read-answer flow in one pass:

import requests

def web_context(query, k=3):
    r = requests.post(
        "https://api.link.sc/v1/search",
        headers={"x-api-key": "lsc_YOUR_KEY"},
        json={"q": query},
    )
    results = r.json()["results"][:k]
    return results  # each has title, url, and full markdown content

def answer(question):
    sources = web_context(question)
    context = "\n\n".join(
        f"[{i+1}] {s['title']} ({s['url']})\n{s['content']}"
        for i, s in enumerate(sources)
    )
    prompt = (
        f"Answer the question using only the sources below. "
        f"Cite sources by their [number].\n\n"
        f"Sources:\n{context}\n\nQuestion: {question}"
    )
    return call_your_llm(prompt)

And the same idea in Node:

async function webContext(query) {
  const res = await fetch("https://api.link.sc/v1/search", {
    method: "POST",
    headers: {
      "x-api-key": "lsc_YOUR_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ q: query }),
  });
  const { results } = await res.json();
  return results; // title, url, full markdown content per result
}

Citing Your Sources

Because each result carries its own URL, you get citations almost for free. Number the sources in the prompt (as above), tell the model to cite by number, and then map those numbers back to URLs when you render the answer. This does two things: it lets users verify claims, and it makes hallucinations obvious — if the model asserts something that isn't in any cited source, that's a red flag you can catch in review or with a follow-up check.

Grounding plus citations is the difference between a demo and something you'd let a user trust. The model isn't recalling; it's reading sources you can point at.

Wrapping Up

Real-time search for LLMs comes down to a simple pipeline — search, read the full pages, answer with citations — where "read the full pages" is the step everyone underestimates. Returning full-page markdown alongside search results is what lets you skip the fetch tax and keep the flow to a single call.

link.sc gives you 500 free credits a month to build this out. Grab a key at link.sc/register, or check the docs for the full /search and /fetch reference. It's open core if you'd prefer to self-host.