Resolve scholarly identifiers (DOI, PMID, PMCID, ISBN, arXiv, ISSN, ADS bibcode, WHO IRIS URL) into formatted citations (10,000+ CSL styles) and bibliography exports (BibTeX, RIS, EndNote, CSV…), verify a claimed citation against the record its identifier actually resolves to, and audit a reference list of any length for fabricated and retracted entries — from Python via the `scholar-sidekick` package. Wraps the public REST API; needs Python ≥3.9 but no API key for the free tier. Use when the agent runs Python (a script, a notebook, a data pipeline) and the user wants to cite, export, verify, or audit references by DOI, PMID, PMCID, ISBN, arXiv ID, ISSN, ADS bibcode, or WHO IRIS URL — especially a bibliography longer than 25 entries, which this client chunks and paces automatically.
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Turn a scholarly identifier into a formatted citation, a bibliography file, or a fabrication
check, using the scholar-sidekick package. It is a thin, typed client over the public Scholar
Sidekick REST API. No API key required for the free, rate-limited tier. Results come back as
objects, so you never parse text output.
Sibling skills, same API underneath: use
scholar-sidekick-apiwhen the agent only hascurl,scholar-sidekick-clifor a Node ≥20 terminal, orscholar-sidekick-mcpwhen an MCP host is connected. This skill is the Python path.
verify() compares the claimed title against the
record the identifier resolves to.audit_bibliography() chunks and paces itself; hand-rolled loops hit the rate limit.This client does not cover everything the REST API does. There is no standalone retraction
check, no open-access check, no style search, and no bare identifier resolution. For those, use the
scholar-sidekick-api or scholar-sidekick-cli skill. Retraction screening is available, but
only inside an audit.
Published on PyPI as scholar-sidekick (import name scholar_sidekick). Requires Python ≥ 3.9.
pip install scholar-sidekick
pip install 'scholar-sidekick[pandas]' # adds report.to_dataframe()| Method | Purpose | Returns |
|---|---|---|
format() | Resolve identifiers and format them | FormatResult (.text, .html) |
format_items() | Format already-resolved CSL-JSON items | FormatItemsResult (.output) |
export() | Export to a bibliography file format | str — the file body |
verify() | Check one claimed citation against the resolved record | VerifyResult |
audit() | Audit up to 25 references in one call | AuditReport |
audit_bibliography() | Audit any number, chunked and paced | AuditReport |
health() | Service liveness | dict |
AsyncScholarSidekick mirrors every method; only the awaiting differs.
from scholar_sidekick import ScholarSidekick
client = ScholarSidekick()
result = client.verify(
title="Quantum entanglement in biological systems",
doi="10.1038/nphys1170",
)
print(result.verdict, result.confidence)title is required, plus at least one identifier keyword (doi, pmid, pmcid, isbn,
arxiv, issn, ads, who_iris_url). Optional: authors, year, container.
| verdict | meaning |
|---|---|
matched | the claimed citation agrees with the resolved record |
mismatch | the identifier resolves to a different work — the dominant fabrication pattern (real DOI, invented title; Topaz et al., Lancet 2026) |
ambiguous | a discrepancy a human should read; not an accusation |
not_found | no record found in the registries searched |
Two rules that matter:
mismatch is returned as a value, never raised. It is the expected outcome for a
fabricated citation, and the reason this method exists.not_found is not proof of fabrication. Standards documents, software repositories, model
cards and institutional reports are often real but absent from scholarly registries. Report it as
"could not confirm".Do not use format() to answer "is this real?". A fabricated citation carries a real, resolvable
DOI, so formatting it succeeds and proves nothing. Only verify() compares the claimed title.
claims = [
{"title": "Attention is all you need", "arxiv": "1706.03762"},
{"title": "Ileal-lymphoid-nodular hyperplasia…", "doi": "10.1016/S0140-6736(97)11096-0"},
# …any number of entries
]
report = client.audit_bibliography(
claims,
progress=lambda done, total: print(f"{done}/{total} chunks"),
)
print(report.summary) # total / matched / mismatch / ambiguous / not_found / errored / retracted
for entry in report.needs_review:
print(entry.input_index, entry.verdict, entry.claimed_title, entry.resolved_title)entry.input_index is the 0-based position in the list you passed in, whatever the chunking
did. Use it to point at the right reference.retracted, has_concern, has_corrections, and notices — retraction
screening is on by default. Pass checks=[] to skip it. "retraction" is the only accepted
check value.if not report.complete:
for failure in report.errors:
print(f"entries {failure.start_index}-{failure.end_index} failed: {failure.error}")Pass stop_on_error=True to raise on the first failed chunk instead.
Claim dicts are strict — the server rejects unknown keys. Allowed: title, authors
([{"family": …, "given": …}], max 50), year (int), container, and the identifiers doi,
pmid, pmcid, isbn, arxiv, issn, ads, whoIrisUrl (note the camelCase). Strip anything
else before sending.
Two input kinds, and the wrong one fails. Default kind="claims" expects dicts. For raw prose
reference strings — a pasted reference list with no parsed title — pass kind="references", which
verifies by containment instead of title comparison:
report = client.audit_bibliography(reference_strings, kind="references")With pandas installed: report.to_dataframe().
Use audit() instead of audit_bibliography() only when you have 25 entries or fewer and want a
single call. audit() also accepts a raw bibliography= string (BibTeX / RIS / CSL-JSON) which
the server parses; pass format= to override auto-detection.
This audits citation identity. It does not check whether a source supports the claim it is cited for.
print(client.format(text="10.1038/nphys1170", style="vancouver").text)
with open("refs.bib", "w") as fh:
fh.write(client.export(text="10.1038/nphys1170 PMID:30049270", format="bibtex"))format() takes exactly one of text= (a blob the API detects identifiers in) or
lines= (a sequence). Passing both, or neither, raises ValueError.style: vancouver (default), ama, apa, ieee, cse, or any CSL style ID
(chicago-author-date, nature, the-lancet, …).export() formats: bibtex, ris, csv, csl, endnote-xml, endnote-refer, refworks,
medline, zotero-rdf, txt. It returns the file body as a string — write it yourself.Exceptions are chosen by the API's stable error code, not by HTTP status.
from scholar_sidekick import APIError, RateLimitError, UpstreamError
try:
client.format(text="10.1038/nphys1170")
except RateLimitError as exc:
print("retry after", exc.retry_after)
except UpstreamError as exc:
print("a data source failed:", exc.code)
except APIError as exc:
print(exc.code, exc.message, exc.request_id)| Exception | When |
|---|---|
ValidationError | the request was malformed or invalid |
AuthError | authentication or entitlement was refused |
RateLimitError | rate limit exceeded; carries .retry_after |
UpstreamError | a data source failed after the API's own retries |
TransportError | connection failure or timeout; no HTTP response |
Every APIError carries .request_id. Include it when reporting a problem.
On any exception, report the failure. Never invent a citation, a retraction status, or a
matched verdict the client did not return.
Works anonymously at the free, rate-limited tier — fine for normal agent use. A key raises the limit about fivefold but is never required:
client = ScholarSidekick(api_key="ssk_…")Precedence: the explicit argument, then SCHOLAR_SIDEKICK_API_KEY, then anonymous. Free keys come
from https://scholar-sidekick.com/account. Other constructor options: base_url, timeout
(30 s), connect_timeout (10 s), max_retries (2). SCHOLAR_SIDEKICK_BASE_URL overrides the host.
Both clients close cleanly as context managers:
with ScholarSidekick() as client:
...
async with AsyncScholarSidekick() as client:
result = await client.verify(title="…", doi="10.1038/nphys1170")resolve(), no retraction method, no open-access method, no style search. Only the seven
methods above exist. Use the scholar-sidekick-api or -cli skill for the rest.format() returns whole-batch .text/.html covering every input, newline-joined — not one
string per item. Do not split it to synthesise per-item citations; styles put newlines inside
entries. format_items() names the same string .output, deliberately.AuditEntry has claimed_title and resolved_title, not .title.kind="claims" needs dicts; raw strings need kind="references".audit() requires exactly one of bibliography=, claims=, or references= — it raises
ValueError locally rather than spending a round trip.audit_bibliography() sends chunks serially even in the async client. That is deliberate:
the constraint is the server's upstream budget, not local concurrency. Do not parallelise it.PMID:, arXiv:, ISBN hyphens and https://doi.org/… are all
tolerated.from scholar_sidekick import ScholarSidekick
client = ScholarSidekick()
assert client.health().get("ok") is True
# A real DOI with a deliberately wrong title must come back `mismatch`.
print(client.verify(title="A title this paper does not have",
doi="10.1038/nphys1170").verdict)