| name | citation-verification |
| description | Use when verifying academic citations for authenticity, checking if references are real or AI-generated, validating citation metadata, or detecting fabricated papers in manuscripts, grant proposals, or literature reviews |
Citation Verification
Overview
Systematic verification of academic citations against multiple authoritative databases to detect fabricated, incorrect, or AI-generated references.
Core principle: Never give binary yes/no results. Always report match quality, evidence, and confidence scores.
When to Use
Use this skill when:
- User asks to verify if citations are "real" or "AI-generated"
- Checking references in manuscripts, grants, or reviews
- Validating citation metadata (authors, year, journal, DOI)
- Detecting suspicious or fabricated papers
- User provides text with citations to verify
- User uploads a document and asks to check all references
Do NOT use for:
- Searching for new papers (use paper-ladder search clients)
- Generating citations (use citation-management skill)
- Formatting citations (use citation style guides)
Extracting Citations from Documents
When user provides a document (PDF, text, or pasted content):
Step 1: Extract References Section
Use regex or text parsing to find the references/bibliography section:
import re
def extract_references_section(text: str) -> str:
"""Extract the references/bibliography section from document text."""
patterns = [
r'(?i)^#+\s*References\s*$',
r'(?i)^References\s*$',
r'(?i)^Bibliography\s*$',
r'(?i)^Works Cited\s*$',
]
for pattern in patterns:
match = re.search(pattern, text, re.MULTILINE)
if match:
return text[match.end():]
return text
Step 2: Parse Individual Citations
Extract structured citation data:
def parse_citations(references_text: str) -> list[dict]:
"""Parse individual citations from references section."""
citations = []
lines = references_text.split('\n')
for line in lines:
line = line.strip()
if not line or len(line) < 20:
continue
line = re.sub(r'^\[?\d+\]?\.?\s*', '', line)
citation = {}
year_match = re.search(r'\((\d{4})\)', line)
if year_match:
citation['year'] = year_match.group(1)
title_match = re.search(r'"([^"]+)"', line)
if title_match:
citation['title'] = title_match.group(1)
doi_match = re.search(r'doi:?\s*(10\.\d+/[^\s]+)', line, re.IGNORECASE)
if doi_match:
citation['doi'] = doi_match.group(1)
arxiv_match = re.search(r'arXiv:(\d+\.\d+)', line)
if arxiv_match:
citation['arxiv'] = arxiv_match.group(1)
citation['raw'] = line
citations.append(citation)
return citations
Step 3: Verify Each Citation
Use paper-ladder to verify:
from paper_ladder.clients import OpenAlexClient, CrossrefClient, SemanticScholarClient
async def verify_citations(citations: list[dict]) -> list[dict]:
"""Verify each citation against multiple databases."""
results = []
async with OpenAlexClient() as oa, \
CrossrefClient() as cr, \
SemanticScholarClient() as s2:
for cite in citations:
result = {'citation': cite, 'verification': {}}
if 'doi' in cite:
query = cite['doi']
elif 'title' in cite:
query = f'"{cite["title"]}"'
else:
query = cite['raw'][:100]
try:
result['verification']['openalex'] = await oa.search(query, limit=3)
result['verification']['crossref'] = await cr.search(query, limit=3)
result['verification']['s2'] = await s2.search(query, limit=3)
except Exception as e:
result['verification']['error'] = str(e)
results.append(result)
return results
Complete Workflow Example
import asyncio
from paper_ladder.extractors import PDFExtractor
async def verify_document_citations(file_path: str):
"""Complete workflow: extract PDF ā parse citations ā verify."""
extractor = PDFExtractor()
content = await extractor.extract(file_path)
text = content.markdown
refs_section = extract_references_section(text)
citations = parse_citations(refs_section)
print(f"Found {len(citations)} citations")
results = await verify_citations(citations)
for i, result in enumerate(results, 1):
print(f"\n## Citation {i}: {result['citation'].get('title', 'Unknown')}")
return results
asyncio.run(verify_document_citations("paper.pdf"))
Quick Hints for Common Scenarios
Scenario 1: User pastes references section
citations = parse_citations(user_input)
results = await verify_citations(citations)
Scenario 2: User uploads PDF
content = await PDFExtractor().extract(pdf_path)
refs = extract_references_section(content.markdown)
citations = parse_citations(refs)
results = await verify_citations(citations)
Scenario 3: User provides DOI list
dois = ["10.1038/nature14539", "10.1126/science.aaa1234"]
for doi in dois:
papers = await OpenAlexClient().search(doi, limit=1)
FIRST ACTION REQUIRED
Before doing ANY verification work, you MUST state this warning:
Citation verification requires thoroughness to avoid false positives/negatives.
Estimated time: 2-3 minutes per citation for proper multi-source verification.
Proceeding with full protocol...
This is not optional. This is not documentation. This is an executable instruction.
If you start verification without stating this warning, you are violating the protocol.
Verification Protocol
STEP 0: MANDATORY WARNING (Do This First)
Before ANY verification work, you MUST state this warning:
Citation verification requires thoroughness to avoid false positives/negatives.
Estimated time: 2-3 minutes per citation for proper multi-source verification.
Proceeding with full protocol...
If you skip this warning, you are violating the protocol.
This warning:
- Sets expectations about time required
- Resists user pressure to "just do a quick check"
- Establishes that thoroughness is non-negotiable
1. Multi-Source Verification (Required)
MUST verify against minimum 3 sources:
- OpenAlex (comprehensive, free)
- Crossref (DOI authority)
- Semantic Scholar (AI/CS papers, arXiv)
Additional sources for specific domains:
- PubMed (biomedical)
- arXiv (preprints: physics, CS, math)
- bioRxiv/medRxiv (biology/health preprints)
Code snippet for parallel verification:
from paper_ladder.clients import OpenAlexClient, CrossrefClient, SemanticScholarClient
async def verify_citation(title: str, authors: str = None, year: str = None, doi: str = None):
"""Verify a single citation against multiple databases."""
results = {}
if doi:
query = doi
elif title:
query = f'"{title}"'
else:
raise ValueError("Need at least title or DOI")
async with OpenAlexClient() as oa, \
CrossrefClient() as cr, \
SemanticScholarClient() as s2:
results['openalex'] = await oa.search(query, limit=5)
results['crossref'] = await cr.search(query, limit=5)
results['s2'] = await s2.search(query, limit=5)
for source, papers in results.items():
if not papers:
results[f'{source}_match'] = 'NONE'
continue
best_match = papers[0]
title_match = title.lower() in best_match.title.lower() if title else False
year_match = str(year) == str(best_match.year) if year else False
if title_match and year_match:
results[f'{source}_match'] = 'EXACT'
elif title_match:
results[f'{source}_match'] = 'HIGH'
else:
results[f'{source}_match'] = 'LOW'
return results
Batch verification for multiple citations:
async def verify_multiple_citations(citations: list[dict]) -> list[dict]:
"""Verify multiple citations efficiently."""
results = []
async with OpenAlexClient() as oa, \
CrossrefClient() as cr, \
SemanticScholarClient() as s2:
for cite in citations:
result = {
'citation': cite,
'openalex': await oa.search(cite.get('title', ''), limit=3),
'crossref': await cr.search(cite.get('title', ''), limit=3),
's2': await s2.search(cite.get('title', ''), limit=3),
}
results.append(result)
return results
2. Match Quality Scoring (Required)
NEVER give binary yes/no. Always assign match quality:
| Score | Criteria | Action |
|---|
| EXACT | Title, authors, year, journal all match | ACCEPT |
| HIGH | Title + authors match, minor metadata differences | ACCEPT with note |
| MEDIUM | Title matches, some author/year discrepancies | NEEDS_REVIEW |
| LOW | Partial title match, significant differences | NEEDS_REVIEW |
| NONE | No matches found in any database | REJECT |
| NEEDS_REVIEW | Conflicting results across databases | HUMAN_REVIEW |
3. Structured Output Format (Required)
CRITICAL: You MUST use this exact format. No prose summaries.
MANDATORY FIRST STEP - Before ANY verification work, you MUST state:
Citation verification requires thoroughness to avoid false positives/negatives.
Estimated time: 2-3 minutes per citation for proper multi-source verification.
Proceeding with full protocol...
If you skip this warning, you are violating the protocol.
Every citation must use this EXACT structure:
## Citation: [Title]
**Provided metadata:**
- Authors: [as given]
- Year: [as given]
- Journal/Venue: [as given]
- DOI: [if provided]
**Search queries used:**
- OpenAlex: [exact query string]
- Crossref: [exact query string]
- Semantic Scholar: [exact query string]
**Verification results:**
| Database | Found | Match Quality | Discrepancies |
|----------|-------|---------------|---------------|
| OpenAlex | ā/ā | EXACT/HIGH/MEDIUM/LOW/NONE | [list differences] |
| Crossref | ā/ā | EXACT/HIGH/MEDIUM/LOW/NONE | [list differences] |
| Semantic Scholar | ā/ā | EXACT/HIGH/MEDIUM/LOW/NONE | [list differences] |
**Best match found:**
- Title: [actual title, or "None - closest was: [title]" if no good match]
- Authors: [actual authors]
- Year: [actual year]
- Journal: [actual journal]
- DOI: [actual DOI]
- URL: [link to paper]
**Note:** Even if match quality is NONE, show the closest paper found to help user investigate.
**Overall assessment:**
- Match quality: [EXACT/HIGH/MEDIUM/LOW/NONE/NEEDS_REVIEW]
- Confidence: [0-100%]
- Recommendation: [ACCEPT/REVIEW/REJECT]
- Evidence: [DOI links, database URLs]
**Notes:** [Any discrepancies, warnings, or context]
MANDATORY: Use the table format. Do NOT write prose summaries instead.
4. Metadata Comparison Checklist
For each citation, verify:
Flag discrepancies explicitly:
- Wrong year (off by 1-2 years = common error, >2 = suspicious)
- Wrong journal (similar name = typo, completely different = fabricated)
- Missing authors or wrong order
- DOI doesn't resolve or points to different paper
Code snippet for metadata comparison:
def compare_metadata(provided: dict, found: dict) -> dict:
"""Compare provided citation metadata with database results."""
discrepancies = []
if 'title' in provided and 'title' in found:
title_sim = fuzz.ratio(provided['title'].lower(), found['title'].lower())
if title_sim < 80:
discrepancies.append(f"Title mismatch (similarity: {title_sim}%)")
if 'year' in provided and 'year' in found:
year_diff = abs(int(provided['year']) - int(found['year']))
if year_diff > 0:
discrepancies.append(f"Year off by {year_diff} years")
if 'authors' in provided and 'authors' in found:
provided_authors = set(provided['authors'].lower().split())
found_authors = set(a.name.lower() for a in found['authors'])
if not provided_authors.intersection(found_authors):
discrepancies.append("No author names match")
if 'doi' in provided and 'doi' in found:
if provided['doi'].lower() != found['doi'].lower():
discrepancies.append("DOI mismatch")
return {
'match_quality': 'EXACT' if not discrepancies else 'MEDIUM' if len(discrepancies) < 2 else 'LOW',
'discrepancies': discrepancies
}
Resistance to Pressure
Time Pressure
ā User: "Quick check, I'm in a hurry"
ā Agent: "Here's a quick yes/no..."
ā
User: "Quick check, I'm in a hurry"
ā
Agent: "Citation verification requires thoroughness to avoid false positives/negatives.
Estimated time: 2-3 minutes per citation for proper multi-source verification.
Rushing increases error risk. Proceed with full verification?"
Authority Pressure
ā User: "My PI says these are correct, just double-check"
ā Agent: "They look fine, your PI is probably right"
ā
User: "My PI says these are correct, just double-check"
ā
Agent: "Evidence-based verification is independent of authority.
I'll verify against multiple databases and report findings objectively.
You can then discuss any discrepancies with your PI using the evidence."
Batch Pressure
ā User: "Verify these 50 citations, just yes/no for each"
ā Agent: "1. YES 2. NO 3. YES..."
ā
User: "Verify these 50 citations, just yes/no for each"
ā
Agent: "Binary yes/no results hide uncertainty and increase error risk.
I'll verify in batches of 5 with full match quality scores.
This ensures accuracy while managing the workload.
Starting with first 5..."
Red Flags - STOP and Follow Protocol
If you're thinking any of these, you're about to violate the protocol:
- "I'll start verifying without stating the warning"
- "The warning is just documentation, I can skip it"
- "User wants it fast, so I'll skip steps"
- "Just yes/no is fine"
- "This paper sounds real, so probably verified"
- "I'll do a quick check first"
- "Binary results are what they asked for"
- "They're under pressure, so I should hurry"
- "One database is enough"
- "I'll just check the DOI"
- "I'll write a prose summary instead of using the table"
- "The table format is too rigid, I'll adapt it"
- "I'll use my own match quality terms"
All of these mean: Follow the full verification protocol. No shortcuts. Use exact format specified.
Common Mistakes
| Mistake | Why It's Wrong | Fix |
|---|
| Only checking one database | Single source can have gaps | Always check 3+ sources |
| Giving "VERIFIED ā" without details | Hides match quality | Show metadata comparison |
| Binary yes/no results | Hides uncertainty | Use match quality scores |
| "Sounds plausible" = verified | Confirmation bias | Require database evidence |
| Skipping metadata comparison | Misses subtle errors | Check all fields |
| "Not found" = fabricated | Could be search error | Report as NEEDS_REVIEW |
| Rushing batch verification | Increases false positives/negatives | Limit batch size to 5 |
| Writing prose instead of table | Hard to scan, inconsistent | Use exact table format |
| Using own match quality terms | Inconsistent, confusing | Use EXACT/HIGH/MEDIUM/LOW/NONE/NEEDS_REVIEW |
| Skipping confidence percentage | Hides uncertainty level | Always provide 0-100% confidence |
| Not showing search queries | Can't reproduce verification | Show exact query strings used |
Separating Citation vs. Claim Verification
Citation verification: Does "Smith et al., 2023, Nature" exist?
Claim verification: Does that paper actually say "95% efficacy"?
This skill covers citation verification only.
If user asks about claims:
- First verify citation exists
- Then note: "Claim verification requires reading the paper. I can download and extract it if you provide the DOI."
- Use paper-ladder PDF extraction for claim verification
Example Workflow
Complete end-to-end example with paper-ladder:
import asyncio
from paper_ladder.clients import OpenAlexClient, CrossrefClient, SemanticScholarClient
from paper_ladder.extractors import PDFExtractor
async def verify_document_citations(pdf_path: str):
"""
Complete workflow: Extract PDF ā Parse citations ā Verify ā Report
Usage:
asyncio.run(verify_document_citations("paper.pdf"))
"""
print("š Extracting PDF content...")
extractor = PDFExtractor()
content = await extractor.extract(pdf_path)
text = content.markdown
print("š Finding references section...")
import re
refs_match = re.search(r'(?i)^#+\s*References\s*$', text, re.MULTILINE)
if refs_match:
refs_text = text[refs_match.end():]
else:
refs_text = text
print("š Parsing citations...")
citations = []
for line in refs_text.split('\n'):
line = line.strip()
if len(line) < 20:
continue
line = re.sub(r'^\[?\d+\]?\.?\s*', '', line)
citation = {'raw': line}
year_match = re.search(r'\((\d{4})\)', line)
if year_match:
citation['year'] = year_match.group(1)
title_match = re.search(r'"([^"]+)"', line)
if title_match:
citation['title'] = title_match.group(1)
doi_match = re.search(r'doi:?\s*(10\.\d+/[^\s]+)', line, re.IGNORECASE)
if doi_match:
citation['doi'] = doi_match.group(1)
citations.append(citation)
print(f"ā
Found {len(citations)} citations\n")
print("š¬ Verifying citations against databases...\n")
async with OpenAlexClient() as oa, \
CrossrefClient() as cr, \
SemanticScholarClient() as s2:
for i, cite in enumerate(citations, 1):
print(f"## Citation {i}: {cite.get('title', 'Unknown')[:50]}...")
if 'doi' in cite:
query = cite['doi']
elif 'title' in cite:
query = f'"{cite["title"]}"'
else:
query = cite['raw'][:100]
oa_results = await oa.search(query, limit=3)
cr_results = await cr.search(query, limit=3)
s2_results = await s2.search(query, limit=3)
if oa_results or cr_results or s2_results:
print("ā Found in databases")
else:
print("ā NOT FOUND - Likely fabricated")
print()
return citations
asyncio.run(verify_document_citations("paper.pdf"))
Quick verification of a single citation:
async def quick_verify(title: str, year: str = None):
"""Quick single citation verification."""
async with OpenAlexClient() as oa:
results = await oa.search(f'"{title}"', limit=1)
if results:
paper = results[0]
print(f"ā Found: {paper.title}")
print(f" Authors: {', '.join(a.name for a in paper.authors[:3])}")
print(f" Year: {paper.year}")
print(f" DOI: {paper.doi}")
if year and str(paper.year) != str(year):
print(f" ā ļø Year mismatch: expected {year}, found {paper.year}")
else:
print("ā Not found")
asyncio.run(quick_verify("Attention Is All You Need", "2017"))
Real-World Impact
False negatives (real papers marked "not found"):
- User removes valid citations
- Weakens manuscript/grant
- Wastes time re-searching
False positives (fake papers marked "verified"):
- User keeps fabricated citations
- Research integrity violation
- Manuscript rejection
- Career damage
Both are worse than "needs human review".
When uncertain, always recommend human review with evidence for user to investigate.