To give a local LLM live web search, fetch real results before the model answers: send the user's question to Scavio's Google endpoint, take the top organic titles, snippets, and links, paste them into a context block, and tell Ollama to answer only from those sources and cite the links. That's the whole trick. A local model has no internet and a frozen training cutoff, so left alone it guesses. Worse, in 2026 most local models still think it's 2024, even though every search result you hand them comes back dated 2026, so you also have to feed today's date in the prompt or they argue with the evidence. This is retrieval-augmented generation, just pointed at a SERP instead of a vector database. The loop below runs end to end in Python on your laptop: Ollama on http://localhost:11434, one POST to https://api.scavio.dev/api/v1/google, and a grounding prompt that forces citations. Scavio charges $0.005 per credit and a light Google request is 1 credit, so a thousand grounded questions cost about five dollars with no infrastructure to run. We also cover the honest alternative: self-hosting SearxNG plus a crawler is free and private, but you own the containers, proxies, and uptime. Brave's roughly 1,000 free searches a month is a fine zero-cost start for hobby use.
Prerequisites
- Ollama installed and running locally with a pulled model (for example, llama3.1 or qwen2.5) on http://localhost:11434
- Python 3.9+ with the requests library (pip install requests)
- A Scavio API key from scavio.dev, exported as SCAVIO_API_KEY
- Basic familiarity with the Ollama chat API and shell environment variables
Walkthrough
Step 1: Fetch live results from Scavio's Google endpoint
Send the user's question to POST /api/v1/google with Bearer auth. Use light_request: false only when you need the full SERP (people_also_ask, knowledge_graph); a light request is cheaper at 1 credit. You get back structured organic_results with position, title, link, and snippet, so there's nothing to scrape or parse out of HTML.
import os, requests
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}",
"Content-Type": "application/json"}
def search(query, k=5):
r = requests.post("https://api.scavio.dev/api/v1/google", headers=H,
json={"query": query, "light_request": True})
r.raise_for_status()
return r.json().get("organic_results", [])[:k]Step 2: Build a grounding context block with today's date
Turn the top results into a compact, numbered context string. Inject the current date so the model stops insisting it's 2024. Keep each snippet short; trimming here is what keeps the prompt small and the token cost low.
from datetime import date
def build_context(results):
today = date.today().isoformat()
lines = [f"Today's date is {today}.", "", "Sources:"]
for i, row in enumerate(results, 1):
title = row.get("title", "")
snippet = (row.get("snippet", "") or "")[:280]
link = row.get("link", "")
lines.append(f"[{i}] {title}\n{snippet}\n{link}")
return "\n".join(lines)Step 3: Send the grounded prompt to Ollama and force citations
Post the context plus the question to the local Ollama chat API. The system message is the guardrail: answer only from the sources, say so when they don't cover the question, and cite the [n] markers. stream is false so you get one complete JSON response.
def ask_local(question, context, model="llama3.1"):
system = ("Answer ONLY from the provided sources. "
"If they don't contain the answer, say you don't know. "
"Cite sources inline as [1], [2]. Trust the dates in the sources.")
payload = {"model": model, "stream": False, "messages": [
{"role": "system", "content": system},
{"role": "user", "content": f"{context}\n\nQuestion: {question}"}]}
r = requests.post("http://localhost:11434/api/chat", json=payload)
r.raise_for_status()
return r.json()["message"]["content"]Step 4: Run the full loop and handle failure modes
Chain the three steps. Handle the cases that actually happen: zero results (tell the model so, don't fake an answer), HTTP 429 from the free/payg 1 request-per-second rate limit (back off and retry), and a stale cache (rephrase or add a recency term). Never let the model answer from nothing.
import time
def grounded_answer(question, model="llama3.1"):
try:
results = search(question)
except requests.HTTPError as e:
if e.response.status_code == 429:
time.sleep(1.1) # free/payg: 1 req/sec
results = search(question)
else:
raise
if not results:
return "No live results found. I won't guess."
ctx = build_context(results)
return ask_local(question, ctx, model)
if __name__ == "__main__":
print(grounded_answer("What changed in Python 3.13 free-threading?"))Python Example
import os, time, requests
from datetime import date
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}",
"Content-Type": "application/json"}
OLLAMA = "http://localhost:11434/api/chat"
def search(query, k=5):
r = requests.post("https://api.scavio.dev/api/v1/google", headers=H,
json={"query": query, "light_request": True})
r.raise_for_status()
return r.json().get("organic_results", [])[:k]
def build_context(results):
today = date.today().isoformat()
lines = [f"Today's date is {today}.", "", "Sources:"]
for i, row in enumerate(results, 1):
snippet = (row.get("snippet", "") or "")[:280]
lines.append(f"[{i}] {row.get('title','')}\n{snippet}\n{row.get('link','')}")
return "\n".join(lines)
def ask_local(question, context, model="llama3.1"):
system = ("Answer ONLY from the provided sources. "
"If they don't contain the answer, say you don't know. "
"Cite sources inline as [1], [2]. Trust the dates in the sources.")
payload = {"model": model, "stream": False, "messages": [
{"role": "system", "content": system},
{"role": "user", "content": f"{context}\n\nQuestion: {question}"}]}
r = requests.post(OLLAMA, json=payload)
r.raise_for_status()
return r.json()["message"]["content"]
def grounded_answer(question, model="llama3.1"):
try:
results = search(question)
except requests.HTTPError as e:
if e.response.status_code == 429:
time.sleep(1.1) # free/payg: 1 req/sec
results = search(question)
else:
raise
if not results:
return "No live results found. I won't guess."
return ask_local(question, build_context(results), model)
if __name__ == "__main__":
print(grounded_answer("Latest stable Node.js LTS version and its release date?"))JavaScript Example
// Node 18+; Ollama running on localhost:11434
const H = {
Authorization: `Bearer ${process.env.SCAVIO_API_KEY}`,
"Content-Type": "application/json",
};
async function search(query, k = 5) {
const r = await fetch("https://api.scavio.dev/api/v1/google", {
method: "POST",
headers: H,
body: JSON.stringify({ query, light_request: true }),
});
if (!r.ok) throw new Error(`Scavio ${r.status}`);
const data = await r.json();
return (data.organic_results || []).slice(0, k);
}
function buildContext(results) {
const today = new Date().toISOString().slice(0, 10);
const lines = [`Today's date is ${today}.`, "", "Sources:"];
results.forEach((row, i) => {
const snippet = (row.snippet || "").slice(0, 280);
lines.push(`[${i + 1}] ${row.title || ""}\n${snippet}\n${row.link || ""}`);
});
return lines.join("\n");
}
async function askLocal(question, context, model = "llama3.1") {
const system =
"Answer ONLY from the provided sources. If they don't contain the answer, " +
"say you don't know. Cite sources inline as [1], [2]. Trust the dates in the sources.";
const r = await fetch("http://localhost:11434/api/chat", {
method: "POST",
body: JSON.stringify({
model,
stream: false,
messages: [
{ role: "system", content: system },
{ role: "user", content: `${context}\n\nQuestion: ${question}` },
],
}),
});
if (!r.ok) throw new Error(`Ollama ${r.status}`);
return (await r.json()).message.content;
}
async function groundedAnswer(question) {
const results = await search(question);
if (!results.length) return "No live results found. I won't guess.";
return askLocal(question, buildContext(results));
}
groundedAnswer("What is the latest stable Rust release?").then(console.log);Expected Output
The model returns a short answer grounded in the fetched SERP, with inline [1]/[2] citations and links it can actually point to, for example: "The latest Node.js LTS is 22.x, released in 2026 [1]. The download and changelog confirm the date [2]." followed by the source URLs. When Scavio returns no organic_results, the loop prints "No live results found. I won't guess." instead of a hallucinated answer.