The headline existence-verification skill: given a bibliography in any common
form, it proves entry-by-entry whether each reference actually exists by
querying public scholarly APIs, then maps each result to the canonical AlterLab
citation-hallucination taxonomy. It is the deterministic, network-grounded
companion to the LLM-driven integrity_verification_agent — where that agent
uses WebSearch + judgment, this skill uses authoritative API records and a
reproducible Python script, so every verdict rests on retrieved records and the
same input and source responses yield the same verdicts.
Verify references with the script, not from memory. A model checking citations against what it "remembers" shares the training data that produced the hallucination in the first place, so a fabricated reference that feels right passes undetected; only an external record settles existence.
Quick Start
Verify the citations in references.bib
Check whether these DOIs resolve to the papers I cited
Audit my bibliography for fabricated / hallucinated references
Does this reference list contain any fake citations or retractions?
→ Run scripts/verify_citations.py over the bibliography, read the JSON, then
present a verdict table grouped by severity. State the offline/degraded status
explicitly if the network or a source was unavailable.
When to Use This Skill
- "Verify / check / audit my citations or references exist"
- "Did the AI hallucinate any of these references?" / "Is this DOI fake?"
- "Do these DOIs resolve to the papers I cited?"
- "Check this bibliography for retractions"
- "Does this source actually support the sentence I cite it for?" (SH, abstract level)
- A reproducible, scriptable existence check with an explicit offline mode
Does NOT Trigger
| Scenario | Use Instead |
|---|---|
| Writing or revising the paper itself (its citation-check mode formats citations) | alterlab-paper-writer |
| Finding papers, extracting metadata, or generating BibTeX for new sources | alterlab-citation-mgmt |
| Dead or redirected hyperlinks (link rot) in docs or a reference list | alterlab-link-health |
| Full pre-/post-review integrity gate (citation context, data, originality) | alterlab-research-pipeline |
| Holistic peer review of a manuscript with an editorial decision | alterlab-paper-reviewer |
Grading source quality or predatory venues belongs to alterlab-deep-research
(source_verification_agent). For a whole-manuscript audit in Claude Code where every
flag should be re-checked by independent agents, offer the packaged
/alterlab-workflows:citation-audit workflow (alterlab-research-workflows, roughly
10–40 agents), which runs this skill's checks at scale.
What This Does
For each bibliography entry the script:
- Parses the input (auto-detects BibTeX / a bare DOI-or-arXiv list / free-form references, including one-reference-per-line pastes), extracting title, authors, year, venue, DOI, and arXiv ID. A full reference that merely contains a DOI stays free-form, so its title and authors are kept for the Identifier Hijacking check.
- Resolves identifiers — looks up the cited DOI/arXiv ID directly when present, and asks the doi.org Handle API whether a DOI is registered with any agency.
- Searches by title + first author + year as a fallback across the sources.
- Fuzzy-matches the cited title (difflib
SequenceMatcherratio, default threshold 0.70; a main title that is near-identical to a record stored without its subtitle also counts) and computes author-surname overlap. Among the returned records it keeps the one that matches title, then authors, then year — so a same-titled paper by other authors or another edition does not win. - Flags retractions on the matched work: Crossref
updated-by/update-tonotices of type retraction, withdrawal, or removal (from the publisher or, withsource: retraction-watch, the Retraction Watch database that Crossref serves in its REST API since 2025) and OpenAlexis_retracted. Expressions of concern get a separate flag. - Emits a verdict per entry mapped to the taxonomy below, with a
source_statusmap showing which sources answered, plus a repo-levelsummary.verdict(PASS / PASS_WITH_CONDITIONS / FAIL / UNVERIFIED).
Sources (no key required; current as of 2026-09)
| Source | Endpoint | Used for | Access notes |
|---|---|---|---|
| Crossref | api.crossref.org/works |
DOI resolution, bibliographic search, retraction notices | mailto routes to the polite pool (10 req/s, 3 concurrent) |
| OpenAlex | api.openalex.org/works |
DOI lookup + title search, is_retracted |
mailto is ignored since Feb 2026. DOI lookups are free; searches draw on a daily budget shared per IP without a key (a free key gives 10× — set OPENALEX_API_KEY) |
| Semantic Scholar | api.semanticscholar.org/graph/v1 |
DOI/arXiv resolution, title search | Keyless pool is shared and often returns 429; S2_API_KEY gives a dedicated 1 req/s |
| arXiv | export.arxiv.org/api/query |
arXiv ID resolution, preprint title search | ≥ 3 s between calls (the script throttles) |
| doi.org Handle API | doi.org/api/handles/<doi> |
Is the DOI registered with any agency (Crossref, DataCite, mEDRA…)? | Authoritative for DOI existence |
Keys are optional, read from the environment, sent as headers, and never written
to the report (the report only records whether one was present). A source that
errors (rate limit, 5xx) is recorded in source_status and is never counted as
evidence that a work does not exist.
This skill answers "does the cited work exist, and does its identifier point
to it?" From API metadata alone it cannot establish Semantic Hallucination
(does the source support the claim?); scripts/claim_faithfulness.py gives an
abstract-level SH triage (see below), and full-text claim verification is the
research pipeline's claim_verification_protocol (Phase E).
Verdict Taxonomy (mirrors the canonical Five-Type Taxonomy)
Identical codes and definitions to
alterlab-research-pipeline/agents/integrity_verification_agent.md
(GPTZero × NeurIPS 2025; Ansari, 2026). Severity feeds the same
SERIOUS / MEDIUM / MINOR scale used in the Integrity Report schema.
| Code | Name | Severity | Script trigger |
|---|---|---|---|
verified |
— (exists, matches) | NONE | Title ratio >= threshold AND author overlap OK AND year consistent in >=1 authoritative source |
TF |
Total Fabrication | SERIOUS | Cited DOI is not registered at doi.org, or cited arXiv ID does not exist on arXiv, and no close title match exists; OR (no identifier) no record matches title + authors while Crossref and OpenAlex or Semantic Scholar answered |
PAC |
Partial Attribute Corruption | MEDIUM | Entry found but >=1 metadata field disagrees (year, author overlap < 50%, a cited identifier that does not resolve to it, or a garbled title whose closest record shares the authors — possible mashup) |
IH |
Identifier Hijacking | SERIOUS | Cited DOI/arXiv ID resolved (method=id) but the resolved record's title is unrelated (ratio < threshold) |
PH |
Placeholder Hallucination | SERIOUS | Unresolved template/placeholder ([CITATION NEEDED], \cite{}, et al., YYYY, TODO, forthcoming, in press) — caught pre-network |
SH |
Semantic Hallucination | SERIOUS | Entry resolves but does not support its claim — advisory only here; asserted via claim_faithfulness.py / Phase E |
unverified |
— (could not check) | MEDIUM | Offline, the sources needed for a decision errored, or a registered DOI that no index returned. Never treated as passing. |
A RETRACTED flag is attached (and severity bumped to SERIOUS) whenever Crossref
or OpenAlex marks the matched work as retracted, independent of the existence
verdict; an unrelated closest hit never lends its flag to the cited entry.
TF is a serious accusation, so the script only asserts it on authoritative
evidence. When the deciding sources were rate-limited or down, the entry is
unverified with the reason and source_status, and it goes to the fallback below.
Repo-level verdict
- PASS — every entry
verified, no SERIOUS/MEDIUM flags. - PASS_WITH_CONDITIONS — only
PAC/ MEDIUM metadata issues (fixable), and every entry was checked. - FAIL — any SERIOUS verdict (
TF/IH/PH/ retraction). - UNVERIFIED — nothing SERIOUS was found but at least one entry could not be checked (e.g. an offline run or rate-limited sources). This is not a pass — re-run with network access or an API key, or resolve those entries manually.
Pipeline (how to run it)
1. Locate or capture the bibliography
Accept any of: a .bib file, a .txt list of DOIs/arXiv IDs, a pasted
reference list, or inline text. The script auto-detects the format; override
with --format bibtex|doi|freeform if detection is wrong.
2. Run the verifier
uv run python skills/core/alterlab-citation-verifier/scripts/verify_citations.py \
path/to/references.bib \
--mailto <contact-email> \
--threshold 0.70 \
--out citation_report.json
path/to/references.bibmay also be-(stdin) or inline text.--thresholdtunes the fuzzy title-match ratio (0..1; default 0.70).--offlineskips the network and emitsunverifiedverdicts deliberately.- Omit
--outto print the JSON report to stdout. - Optional:
export OPENALEX_API_KEY=…(free key from openalex.org/settings/api) and/orS2_API_KEY=…before large bibliographies — keyless OpenAlex searches share a small per-IP daily budget, and the keyless Semantic Scholar pool is busy.
The script uses requests if installed, else the Python stdlib (urllib), so it
runs with no extra dependencies in a bare uv environment. Expect roughly 3–10 s
per entry: it spaces arXiv calls 3 s apart and backs off on rate limits.
3. Read the JSON and report
Parse summary.verdict and the per-entry verdict codes. Present:
- The headline verdict and counts (
verdict_counts,severity_counts). - A table of every non-
verifiedentry with its code, severity, anddetail. - For each
TF/IH/PH: quote the cited entry and explain the evidence (e.g. "DOI 10.x resolved to an unrelated paper titled '…'", "DOI not registered at doi.org"). - Any
RETRACTED/EXPRESSION_OF_CONCERNflags, prominently. - For
unverifiedentries: say which sources failed (source_status), relay themanual_instructionsand anysummary.notes(for example, an OpenAlex HTTP 429 means the keyless per-IP budget is spent — a freeOPENALEX_API_KEYfixes it), and run the fallback below.
4. Route fixes
TF/PH→ the reference must be removed or replaced; it does not exist.IH→ the DOI/arXiv ID is wrong; find and substitute the correct identifier.PAC→ correct the specific metadata field(s) named indetail; a possible mashup needs the real source identified before it can stay.RETRACTED→ flag to the author; cite the retraction notice or drop the source.
Graceful Degradation and the Fallback
Failures are never swallowed into a pass:
- A DNS/connection failure raises
NetworkUnavailable; the entry becomesunverifiedwith a populatedmanual_instructionsfield. - A rate-limited or failing source is recorded in
source_status; if the sources needed for a decision did not answer, the entry isunverified, notTF. --offlineforces every networked entry tounverifiedup front (placeholders are still caught locally asPH).- The repo-level verdict becomes
UNVERIFIEDwhenever an entry is unverified and nothing SERIOUS was found.
For each unverified entry, re-run with connectivity or a key if possible;
otherwise look it up with the plugin's crossref / openalex MCP tools when they
are available (setup: mcp_setup.md under skills/core/references), then fall back to WebSearch with
three distinct queries (exact title in quotes; title + first author; first author +
venue + year) and, for a DOI, open https://doi.org/<DOI>. If a record is found, report the verdict it supports
and say it came from the fallback. If nothing is found, classify the entry as
TF (NOT_FOUND). There is no "difficult to verify" outcome: the Lin et al.
mashup this skill was built around slipped through three integrity rounds
precisely because it was parked in that gray zone and never searched.
Claim Faithfulness (SH) — scripts/claim_faithfulness.py
Existence is half of the gate; a real paper cited for something it never said
(the "Frankenstein" pattern) passes every existence check. For (claim, DOI) pairs,
claim_faithfulness.py fetches the cited abstract (Crossref, then OpenAlex) and
returns support / contradict / unsupported:
uv run python skills/core/alterlab-citation-verifier/scripts/claim_faithfulness.py \
--claim "<the sentence as written>" --doi <doi>
uv run python .../claim_faithfulness.py --input pairs.json --json # batch
uv run python .../claim_faithfulness.py --input pairs.json --tier llm --json
- The default
heuristictier is lexical and deliberately abstains (unsupported) rather than guessingsupport; it cannot see role reversals or wrong numbers. Thellmtier asks the model set byALTERLAB_MODEL(seeshared/model_env.md) and falls back to the heuristic, flagged, if theclaudeCLI is unavailable. - It only sees the abstract (
abstract_only: true).unsupportedmeans the abstract does not establish the claim — non-coverage, not refutation. Reportcontradictas SH; sendunsupportedclaims that matter to full-text checking. - Always report existence and faithfulness separately: "the citation is real" and "the citation supports this sentence" are different findings.
Output Shape (excerpt)
{
"tool": "alterlab-citation-verifier/verify_citations.py",
"version": "1.1.0",
"summary": {
"total": 2,
"verdict": "FAIL",
"verdict_counts": {"verified": 1, "TF": 1, "PAC": 0, "IH": 0, "PH": 0, "SH": 0, "unverified": 0},
"severity_counts": {"SERIOUS": 1, "MEDIUM": 0, "MINOR": 0},
"citation_integrity_score": 0.5,
"fabrication_risk_score": 0.5,
"retracted": 0,
"source_errors": {"openalex": 1},
"notes": ["OpenAlex rate limit: the keyless daily budget shared by this IP is spent ..."]
},
"entries": [
{"ref_id": "walters2023", "verdict": "verified", "severity": "NONE",
"title_ratio": 1.0, "author_overlap": 1.0, "matches": [{"source": "crossref"}],
"source_status": {"crossref": "record", "openalex": "record", "semanticscholar": "record", "arxiv": "no_record"}},
{"ref_id": "ghostpaper2021", "verdict": "TF", "severity": "SERIOUS",
"detail": "DOI 10.xxxx/... is not registered with any DOI agency (doi.org Handle API)..."}
]
}
citation_integrity_score and fabrication_risk_score (both 0..1) align with
the Integrity Report schema fields of the same name, so the report can feed
alterlab-research-pipeline's integrity gate directly.
Self-Check Before Reporting
- Did the run reach the network, and which sources answered? If entries are
unverified, name the failing sources fromsource_status; do not imply a pass. - Are there any
RETRACTEDflags? Surface them even on otherwise-verifiedentries. - Did any entry score
IH? Confirm the detail shows an id-resolved mismatch, not a loose title-search coincidence (the script enforces this distinction). - Is the headline verdict consistent with the per-entry codes (any SERIOUS → FAIL;
any
unverifiedwithout SERIOUS → UNVERIFIED)? - Did every
unverifiedentry get the WebSearch fallback and a final verdict?
References
alterlab-research-pipeline/agents/integrity_verification_agent.md— canonical Five-Type Taxonomy, compound-deception patterns, and the Lin et al. (2020) mashup case study this skill is built to catch.alterlab-research-pipeline/references/claim_verification_protocol.md— Phase E full-text claim-vs-source verification (beyond the abstract-level SH triage here).shared/schemas/integrity_report.schema.json— the integrity-report shape whosecitation_integrity_score/fabrication_risk_scorethis skill mirrors.examples/caught_hallucination_walkthrough.md— end-to-end run on a real mashup.- Walters, W. H., & Wilder, E. I. (2023). Fabrication and errors in the bibliographic citations generated by ChatGPT. Scientific Reports, 13, 14045. https://doi.org/10.1038/s41598-023-41032-5
- Ansari, S. (2026). Compound deception in elite peer review: A failure mode taxonomy of 100 fabricated citations at NeurIPS 2025 (arXiv:2602.05930). https://arxiv.org/abs/2602.05930
- Rittman, M. (2025, January 29). Retraction Watch retractions now in the Crossref API [Blog post]. Crossref. https://www.crossref.org/blog/retraction-watch-retractions-now-in-the-crossref-api/
Part of the AlterLab Academic Skills suite.