To build a real-time fact-checker that grounds an LLM on live search, split incoming text into individual claims, keep only the check-worthy ones (numbers, named entities, policy or statistical assertions), search each claim against live Google results, then ask the model to judge supported, contradicted, or unverifiable and cite the actual result links. The grounding step is what stops the hallucinated citations people complain about. As one builder put it after wiring a Google search API into their pipeline: they used it precisely because the model tends to invent sources that don't exist. Once every verdict has to point at a real URL the model just retrieved, fabricated citations mostly stop. This tutorial gives you a runnable pipeline: claim extraction, a check-worthiness filter, a POST to Scavio's /api/v1/google endpoint per claim, and a strict judging prompt that returns a verdict plus citations. It costs 1 credit ($0.005) per claim checked, so a 40-sentence transcript with 12 check-worthy claims runs about $0.06. Be honest about what this does and doesn't do: SERP grounding kills fabricated citations and keeps the model current, but it does not adjudicate truth. You still need source-quality filtering and human judgment, and a claim that's simply absent from search is unverifiable, not false.
Prerequisites
- A Scavio API key (free signup gives 50 credits to test with)
- An LLM API key (any chat-completion model with a JSON-capable response works)
- Python 3.9+ or Node 18+
- Basic familiarity with HTTP requests and JSON
Walkthrough
Step 1: Split text into atomic claims
Break the transcript into individual sentences, then into atomic claims. One sentence can carry several claims; keep them separate so each gets its own search. A naive sentence split is enough to start; swap in a model-based claim splitter later if you need finer granularity.
import re
def split_claims(text: str) -> list[str]:
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
return [s.strip() for s in sentences if len(s.strip()) > 0]Step 2: Keep only check-worthy claims
Most sentences aren't worth checking. Keep the ones that make a falsifiable assertion: numbers, percentages, dates, named entities, or policy and statistical claims. This filter is what keeps your credit spend down, since you only search claims that can actually be verified.
import re
NUM = re.compile(r'\d')
ENTITY = re.compile(r'\b[A-Z][a-z]+\b')
KEYWORDS = ('percent', '%', 'billion', 'million', 'increased', 'decreased', 'banned', 'passed', 'voted', 'rate', 'tax', 'budget')
def is_check_worthy(claim: str) -> bool:
has_number = bool(NUM.search(claim))
has_entity = bool(ENTITY.search(claim))
has_keyword = any(k in claim.lower() for k in KEYWORDS)
return (has_number or has_keyword) and has_entityStep 3: Search each claim against live Google
POST the claim text to /api/v1/google. Pull the top organic results and the people_also_ask entries; those snippets are the evidence the model will reason over. Each call costs 1 credit when light_request is true, 2 credits with the full SERP feature set.
import os, requests
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}", "Content-Type": "application/json"}
def retrieve(claim: str) -> list[dict]:
r = requests.post("https://api.scavio.dev/api/v1/google", headers=H,
json={"query": claim, "light_request": True})
data = r.json()
evidence = []
for row in data.get("organic_results", [])[:5]:
evidence.append({"title": row["title"], "snippet": row.get("snippet", ""), "link": row["link"]})
for paa in data.get("people_also_ask", [])[:3]:
evidence.append({"title": paa.get("question", ""), "snippet": paa.get("snippet", ""), "link": paa.get("link", "")})
return evidenceStep 4: Judge the claim against the evidence
Pass the claim plus the retrieved snippets to an LLM with a strict prompt: return supported, contradicted, or unverifiable, a one-line reason, and citations drawn only from the links you passed in. The rule never invent a source is the whole point. If the evidence doesn't cover the claim, the answer is unverifiable.
PROMPT = '''You are a fact-checking judge. Given a CLAIM and EVIDENCE (search snippets with links), return strict JSON:
{"verdict": "supported|contradicted|unverifiable", "reason": "one sentence", "citations": ["<link from evidence>"]}
Rules: cite ONLY links present in EVIDENCE. Never invent a source. If evidence does not address the claim, verdict is "unverifiable".
CLAIM: {claim}
EVIDENCE: {evidence}'''
def judge(claim, evidence, llm):
msg = PROMPT.format(claim=claim, evidence=evidence)
return llm.complete(msg, response_format="json")Python Example
import os, re, json, requests
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}", "Content-Type": "application/json"}
NUM = re.compile(r'\d')
ENTITY = re.compile(r'\b[A-Z][a-z]+\b')
KEYWORDS = ('percent', '%', 'billion', 'million', 'increased', 'decreased', 'banned', 'passed', 'voted', 'rate', 'tax', 'budget')
def split_claims(text):
return [s.strip() for s in re.split(r'(?<=[.!?])\s+', text.strip()) if s.strip()]
def is_check_worthy(claim):
return (bool(NUM.search(claim)) or any(k in claim.lower() for k in KEYWORDS)) and bool(ENTITY.search(claim))
def retrieve(claim):
r = requests.post("https://api.scavio.dev/api/v1/google", headers=H,
json={"query": claim, "light_request": True})
data = r.json()
ev = [{"title": x["title"], "snippet": x.get("snippet", ""), "link": x["link"]}
for x in data.get("organic_results", [])[:5]]
ev += [{"title": p.get("question", ""), "snippet": p.get("snippet", ""), "link": p.get("link", "")}
for p in data.get("people_also_ask", [])[:3]]
return ev
PROMPT = '''You are a fact-checking judge. Given a CLAIM and EVIDENCE, return strict JSON:
{{"verdict":"supported|contradicted|unverifiable","reason":"one sentence","citations":["<link>"]}}
Cite ONLY links present in EVIDENCE. Never invent a source. No coverage => "unverifiable".
CLAIM: {claim}
EVIDENCE: {evidence}'''
def fact_check(text, llm):
results = []
for claim in split_claims(text):
if not is_check_worthy(claim):
continue
evidence = retrieve(claim)
verdict = llm.complete(PROMPT.format(claim=claim, evidence=json.dumps(evidence)), response_format="json")
results.append({"claim": claim, **json.loads(verdict)})
return results
# transcript = "..."
# for r in fact_check(transcript, my_llm):
# print(r["verdict"], r["claim"], r["citations"])JavaScript Example
const H = {
Authorization: `Bearer ${process.env.SCAVIO_API_KEY}`,
"Content-Type": "application/json",
};
const KEYWORDS = ["percent", "%", "billion", "million", "increased", "decreased", "banned", "passed", "voted", "rate", "tax", "budget"];
const splitClaims = (text) =>
text.trim().split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean);
const isCheckWorthy = (c) =>
(/\d/.test(c) || KEYWORDS.some((k) => c.toLowerCase().includes(k))) && /\b[A-Z][a-z]+\b/.test(c);
async function retrieve(claim) {
const r = await fetch("https://api.scavio.dev/api/v1/google", {
method: "POST",
headers: H,
body: JSON.stringify({ query: claim, light_request: true }),
});
const data = await r.json();
const ev = (data.organic_results || []).slice(0, 5).map((x) => ({ title: x.title, snippet: x.snippet || "", link: x.link }));
(data.people_also_ask || []).slice(0, 3).forEach((p) => ev.push({ title: p.question || "", snippet: p.snippet || "", link: p.link || "" }));
return ev;
}
const PROMPT = (claim, evidence) => `You are a fact-checking judge. Given a CLAIM and EVIDENCE, return strict JSON:
{"verdict":"supported|contradicted|unverifiable","reason":"one sentence","citations":["<link>"]}
Cite ONLY links present in EVIDENCE. Never invent a source. No coverage => "unverifiable".
CLAIM: ${claim}
EVIDENCE: ${JSON.stringify(evidence)}`;
async function factCheck(text, llm) {
const out = [];
for (const claim of splitClaims(text)) {
if (!isCheckWorthy(claim)) continue;
const evidence = await retrieve(claim);
const verdict = await llm.complete(PROMPT(claim, evidence), { responseFormat: "json" });
out.push({ claim, ...JSON.parse(verdict) });
}
return out;
}Expected Output
[
{
"claim": "The new budget increases infrastructure spending by 12 percent next year.",
"verdict": "supported",
"reason": "Two retrieved sources state a 12 percent infrastructure increase in the proposed budget.",
"citations": ["https://example-news.test/budget-2026", "https://example-gov.test/budget-summary"]
},
{
"claim": "Unemployment fell to its lowest level since 1969.",
"verdict": "contradicted",
"reason": "Retrieved sources report the lowest level since 2001, not 1969.",
"citations": ["https://example-stats.test/unemployment"]
},
{
"claim": "The committee met privately on Tuesday to discuss the proposal.",
"verdict": "unverifiable",
"reason": "No retrieved source addresses a private Tuesday meeting.",
"citations": []
}
]