To stop an agent fabricating facts like GitHub stars or follower counts, force it to call a search tool and reason only over the returned JSON, never produce numbers from memory. Give the model one tool that hits Scavio's Google SERP endpoint (POST https://api.scavio.dev/api/v1/google) and Reddit search, then write a system prompt that bans inventing any number, date, or proper noun that didn't come back in the tool response. The model becomes a narrator over sourced data instead of a guesser. This grounds public, indexed facts only. A SERP API can confirm what Google already shows publicly; it cannot read a private dashboard or any metric behind a login, so be explicit with the model about that boundary.
Prerequisites
- A Scavio API key from scavio.dev (free tier gives 50 one-time credits, 1 request/sec)
- Python 3.10+ or Node 18+ with an LLM client that supports tool/function calling
- Basic familiarity with how your LLM exposes tools and parses tool results
- An understanding that the model must be told to never output a number that isn't in the tool JSON
Walkthrough
Step 1: Define the search tool the model is allowed to call
Expose exactly one data-fetch tool to the model: a search function that posts to Scavio's Google SERP endpoint. The tool takes a single query string and returns structured JSON. Keep the schema tight so the model can only ask for a search, not freelance. The point is that any factual claim the agent makes must trace back to a tool call you can audit. If the model wants a GitHub star count, it has to search for it, not recall it.
TOOLS = [{
"type": "function",
"function": {
"name": "web_search",
"description": "Search Google for current public facts. Returns sourced JSON. Use this for any number, date, or name.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "The search query"}
},
"required": ["query"]
}
}
}]Step 2: Implement the tool against Scavio's Google endpoint
When the model calls web_search, run a POST to https://api.scavio.dev/api/v1/google with Authorization: Bearer {API_KEY}. Set light_request to false to get organic results, people_also_ask, knowledge_graph, and related_searches in one call (this costs 2 credits instead of 1). Return only the fields the model needs, with each value paired to its source URL so the model can cite where a number came from.
import requests
def web_search(query: str) -> dict:
r = requests.post(
"https://api.scavio.dev/api/v1/google",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"query": query, "light_request": False},
timeout=30,
)
r.raise_for_status()
data = r.json()
return {
"organic": data.get("organic", [])[:5],
"knowledge_graph": data.get("knowledge_graph"),
"people_also_ask": data.get("people_also_ask", []),
}Step 3: Add Reddit search for community-sourced signal
Some facts live in discussion, not on indexed pages. Add a second tool that posts to https://api.scavio.dev/api/v1/reddit/search for first-hand reports, complaints, and opinions. Treat Reddit results as claims made by people, not verified facts, and tell the model to attribute them ('a Reddit user reported...') rather than state them as ground truth. This keeps the agent honest about where softer signal comes from.
def reddit_search(query: str) -> dict:
r = requests.post(
"https://api.scavio.dev/api/v1/reddit/search",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"query": query},
timeout=30,
)
r.raise_for_status()
return {"posts": r.json().get("posts", [])[:5]}Step 4: Write the grounding system prompt
This is the part that actually stops hallucination. Instruct the model: never output a number, date, version, or proper noun unless it appears verbatim in a tool result. If a search returns nothing, say you couldn't verify it rather than guessing. Cite the source URL next to every figure. State plainly that the search tool covers public indexed data only and cannot see private dashboards or behind-login metrics, so for those the answer is 'I can't access that.'
SYSTEM = '''You are a research agent. Rules:
1. Never state a number, date, version, or named entity unless it came back in a tool result. No recalling from memory.
2. For any factual claim, call web_search first, then narrate ONLY the returned values.
3. Put the source URL next to each figure.
4. If a search returns nothing useful, say "I could not verify this" instead of guessing.
5. The search tool sees public indexed data only. It cannot read private dashboards or login-gated metrics. For those, reply that you cannot access them.'''Step 5: Run the loop and verify the model only narrates returned values
Send the user question with the tools and system prompt, execute any tool calls, feed the JSON back, and let the model write its answer. Then spot-check: every number in the output should be findable in a tool response. If you see a figure that isn't, your prompt is too loose, tighten rule 1. The honest failure mode you want is the agent saying 'I couldn't verify the follower count' rather than confidently printing a wrong one.
messages = [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": "How many GitHub stars does the project X have, and what do people say about it?"},
]
# 1. model -> requests web_search("project X github stars")
# 2. you run web_search, append the JSON as a tool message
# 3. model -> may also call reddit_search for sentiment
# 4. model writes final answer using ONLY those returned values, with source URLsPython Example
import os
import requests
API_KEY = os.environ["SCAVIO_API_KEY"]
def web_search(query: str) -> dict:
r = requests.post(
"https://api.scavio.dev/api/v1/google",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"query": query, "light_request": False},
timeout=30,
)
r.raise_for_status()
data = r.json()
return {
"organic": data.get("organic", [])[:5],
"knowledge_graph": data.get("knowledge_graph"),
"people_also_ask": data.get("people_also_ask", []),
}
def reddit_search(query: str) -> dict:
r = requests.post(
"https://api.scavio.dev/api/v1/reddit/search",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"query": query},
timeout=30,
)
r.raise_for_status()
return {"posts": r.json().get("posts", [])[:5]}
SYSTEM = (
"You are a research agent. Never state a number, date, version, or named "
"entity unless it came back in a tool result; do not recall from memory. "
"For any factual claim, call web_search first and narrate only the returned "
"values, with the source URL next to each figure. If a search returns nothing "
"useful, say you could not verify it. The search tool sees public indexed data "
"only; it cannot read private dashboards or login-gated metrics."
)
if __name__ == "__main__":
# The LLM calls web_search('project X github stars'); you return this JSON;
# the model then narrates ONLY these values. Example of the grounding call:
result = web_search("project X github stars")
print(result["knowledge_graph"])
print([item["link"] for item in result["organic"]])
JavaScript Example
const API_KEY = process.env.SCAVIO_API_KEY;
async function webSearch(query) {
const res = await fetch("https://api.scavio.dev/api/v1/google", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, light_request: false }),
});
if (!res.ok) throw new Error(`Scavio ${res.status}`);
const data = await res.json();
return {
organic: (data.organic || []).slice(0, 5),
knowledge_graph: data.knowledge_graph,
people_also_ask: data.people_also_ask || [],
};
}
async function redditSearch(query) {
const res = await fetch("https://api.scavio.dev/api/v1/reddit/search", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query }),
});
if (!res.ok) throw new Error(`Scavio ${res.status}`);
const data = await res.json();
return { posts: (data.posts || []).slice(0, 5) };
}
const SYSTEM =
"You are a research agent. Never state a number, date, version, or named " +
"entity unless it came back in a tool result; do not recall from memory. " +
"For any factual claim, call web_search first and narrate only the returned " +
"values, with the source URL next to each figure. If a search returns nothing, " +
"say you could not verify it. The search tool sees public indexed data only; " +
"it cannot read private dashboards or login-gated metrics.";
// The LLM calls webSearch('project X github stars'); feed the JSON back so the
// model narrates ONLY these returned values:
webSearch("project X github stars").then((r) => {
console.log(r.knowledge_graph);
console.log(r.organic.map((o) => o.link));
});
Expected Output
{
"organic": [
{
"title": "project X - GitHub",
"link": "https://github.com/org/project-x",
"snippet": "project X is an open-source ... 12.4k stars"
}
],
"knowledge_graph": {
"title": "project X",
"type": "Software repository"
},
"people_also_ask": [
{ "question": "Is project X actively maintained?" }
]
}