Citation verification for AI writing and essay tools
Published September 26, 2026
If you build an AI writing assistant, essay helper or research tool, the reference list is where trust is won or lost. Reviews of tools in this space keep returning to the same complaints: citations that point to papers that don't exist, that don't say what the tool claims, or that are only loosely related. A single fabricated reference in a student's bibliography is enough to make a user abandon a product.
This post describes a verification step you can put between the model and the user.
What "verify" actually means
There are three separate failure modes, and each needs a different check:
| Failure | Check |
|---|---|
| The paper doesn't exist | Look the DOI or title up in a real index |
| The metadata is wrong (year, authors, journal) | Compare the model's fields with the index record |
| The paper doesn't support the claim | Compare the claim with the abstract, or search inside the full text |
A search API covers the first two directly and gives you the material for the third.
A verification loop
For each reference the model produces:
- Search by title (and add
yearFrom/yearToif the year is known) withGET /api/references-search. - Fuzzy-match the best result's title against the reference's title; treat a high similarity plus a matching first author as a hit.
- Overwrite the metadata with the index record's values, so the reference list shows the real DOI, year and journal.
- Flag misses. If nothing matches, mark the reference "unverified" in the UI, or ask the model to replace it.
import difflib, requests
def verify(ref_title: str, year: int | None = None):
params = {"q": ref_title, "limit": 5, "mode": "papers"}
if year:
params.update(yearFrom=year - 1, yearTo=year + 1)
hits = requests.get(
"https://openabstracts.com/api/references-search",
params=params, headers={"x-api-key": "isk_live_YOUR_KEY"}, timeout=30,
).json().get("references", [])
best, score = None, 0.0
for h in hits:
s = difflib.SequenceMatcher(None, ref_title.lower(), h["title"].lower()).ratio()
if s > score:
best, score = h, s
return (best, score) if score >= 0.85 else (None, score)
Checking that a paper supports a claim
Use mode=content to search the full text of the PDFs for the specific phrase or statistic. The response includes highlighted snippets, so you can show the passage next to the citation, which is far more convincing to a reader than a bare reference.
Handling failures gracefully
- Don't silently drop references the user typed themselves; mark them "not found in index" — absence from one index isn't proof a paper doesn't exist.
- Batch and cache. Verify once per unique reference and cache the result; you only pay for searches that return results.
- Prefer recall for the user, precision for the model. Be strict when accepting a model's citation, lenient when helping a user check their own.
Scale and cost
Verifying a 30-reference bibliography is 30 searches. With 100 free searches a month and $1.50 per 1,000 after that, it costs a fraction of a cent per document. Searches that return nothing aren't billed.
Also works inside Claude
If your users work in Claude, they can install the OpenAbstracts citation Skill with the MCP connector so the assistant verifies references itself.
A note on scope: OpenAbstracts indexes registered journals, so it won't contain every paper ever written. Treat "verified" as strong positive evidence and "unverified" as a prompt for a second look, not a verdict.