Product · September 8, 2026 · 8 min read
We let the AI research. We didn't let it pick the sources.
Give a model a web-search tool and it will happily cite a content farm, a 2019 aggregate, and a page that doesn't exist. Zsper's research loop ranks and disqualifies sources before anything is read — and the model can only open a URL that survived. Here's the design, and why the refusals matter more than the results.
There's a moment in every AI writing tool's roadmap where someone says "we should give it web search." It sounds like an unambiguous win. It is not.
Here is what you actually get when you hand a language model a search tool and step back: it searches, it reads whatever is on top, and it writes. On top is frequently a content farm. Or a 2019 aggregate quoting a 2017 survey. Or — my favourite failure — a URL it composed itself, which looks exactly like a real URL, and which it will cite with total confidence.
So when we built research into Zsper, we started from a different question. Not how do we let the model search the web. Who decides which sources are good enough?
The answer we shipped: not the model.
TL;DR: Zsper searches, ranks what comes back, disqualifies the sources that don't clear the bar, and only then reads. The tool that fetches a page will refuse a URL that no ranking admitted — so a link the model invented goes nowhere, structurally, rather than as a matter of good behaviour. The loop runs on a budget with a stopping rule it has to earn. And your own attached sources outrank everything it finds.
The problem isn't finding sources. It's the floor.
Search is easy. Ten results come back for anything.
The hard part is that a language model is a bad judge of source quality, and it is bad in a specific and dangerous way: it is fluent about it. Ask it whether a source is reliable and you get a confident, well-structured paragraph explaining why it is. That paragraph is generated with the same machinery as the rest of the draft. It is not an assessment. It is prose about an assessment.
Meanwhile the actual signals of source quality are almost entirely structural. Is this the primary document or someone's summary of it? Is it a filing, a dataset, a first-party report — or is it a listicle republishing a number three hops from where it was measured? Is the outbound link on the page pointing at a source, or at another page on the same site?
Those are things you can rank deterministically. So we do, before any model sees anything.
Ranking, then reading — in that order
The loop is: plan a query, search it, rank the results and disqualify the ones that don't clear the bar, read what survived, decide whether that's enough, and stop or go again.
The order is the whole design. Ranking happens before reading, which means there is a point in time where the set of admissible URLs is fixed and finite. Everything after that point works from the list.
Which is what makes the next part work.
The tool refuses
fetch_url — the thing that actually goes and gets a page — will not fetch a URL that no ranking admitted.
Not "is instructed not to." Not "is discouraged from." It refuses. The admissible set is checked, and a URL that isn't in it comes back as a refusal.
This closes the invented-URL hole completely, and it closes it in the only way I trust. A rule in a prompt is a request; the model complies with it most of the time, and "most of the time" is a phrase that should worry you when the output has your name on it. A tool that cannot perform the action is not a request. There is no wording that gets around it, no unusual phrasing that slips past, no long conversation that erodes it.
The same boundary does other work. The research loop gets four tools; the connector that plugs Zsper into Claude never gets the web pair at all. Around forty hostile URLs — internal addresses, redirect chains, the whole SSRF greatest-hits collection — get run at the guard as part of our checks, because "fetch this page for me" is a request to make an outbound connection from our infrastructure and should be treated as one.
Corroboration can only add
When a second source agrees with the first, that's worth something. When it disagrees, the temptation is to build a resolution mechanism — a confidence weighting, a majority vote, something that decides who's right.
We deliberately didn't. Corroboration in Zsper can only ever add support. It cannot subtract, cannot override, cannot cause a source to be dropped.
The reason is that an automated arbiter between two disagreeing sources will be wrong occasionally and will be silent about it, and a silent wrong call on a factual disagreement is worse than showing you both. When sources disagree, that disagreement is the interesting thing and it belongs in front of you, not resolved behind your back.
It stops
Every agentic loop's real failure mode is not stopping.
You have seen this: the agent that opens forty tabs and produces a summary of tab thirty-nine, or the one that keeps refining until something times out. The failure is always the same underneath — "enough information" was never defined, so it was never reachable.
Ours runs on a budget. sufficient is a condition that has to be earned, not a flag the planner gets to set about itself. And we test the stopping rule the way it deserves: by driving the real loop with a planner that never wants to stop and an executor that throws errors. It stops anyway. A budget you only test on the happy path is a budget you haven't tested.
There's a cost angle here too, and I'd rather be direct about it. Research burns credits. A loop that reads until it's satisfied, with satisfaction undefined, burns them on your behalf while you're making coffee.
Your sources outrank ours
The best source for a piece is frequently one you already have: a report you were sent, a transcript, a page you want covered specifically.
Paste the link, and Zsper reads it — and here the design flips. A passage from a source you attached is carried verbatim by construction. There's no model call in that path at all: no summarisation step, no proposal, no rewording. It's a read and a quote. It doesn't even cost you credits, because nothing was generated.
If a reference can't be read — the page is gone, it's behind a wall, it's not fetchable — you're told it went unread. It is never quietly dropped. A source list that silently omits its failures is a source list that lies by arithmetic.
And attached material outranks the brain. When you hand over something specific, that is the strongest possible signal about what the piece is for, and the retrieval steps back to make room for it.
Why the refusals are the feature
If you take one thing from this: a research feature with no visible refusals is not doing the work.
Every source that appears in a Zsper piece got there by surviving a ranking. Every source that didn't survive is shown as refused, with the reason. When you look at a sources panel and see three read and one thrown out for being an aggregate with no primary, you are looking at the mechanism operating — and you can disagree with it, which is the point.
A panel that shows only what it accepted is showing you a conclusion. A panel that shows you what it rejected is showing you a judgement, and a judgement is a thing you can argue with.
Zsper writes articles, newsletters and LinkedIn posts from what you actually know — and goes and finds the rest, from sources you can see. Start free for 14 days.
Topics: AI research, citations, web search, source quality, grounded AI
Start building your brain today.
Import what you've written, watch it learn, and ship a draft that sounds like you.
Start writing free