Verischolar is a research-integrity toolkit that runs a multi-stage pipeline to parse scholarly documents, extract claims and citations, build a citation graph, embed uncited claims, and run statistical and heuristic detectors to produce an integrity score and audit report.
backend/: Python backend, ingestion pipeline, and core analysis modules.frontend/: Next.js frontend app for upload, live analysis progress, audit reports, history, and settings.docker-compose.yml: Development compose configuration.
- Document parsing: PDF (
PyMuPDF) and DOCX (python-docx) parsing into page-like text chunks (backend/ingestion/parser.py). - Citation detection: APA parenthetical and narrative detection, numbered citations, DOI extraction, and bibliography/reference-list extraction (
backend/ingestion/citation_detector.py,backend/ingestion/reference_extractor.py). - Claim extraction: LLM-based claim classification via Ollama or OpenAI-compatible APIs with boilerplate heuristics and bounded page-level concurrency (
backend/ingestion/claim_classifier.py,backend/core/langgraph_app.py). - Embeddings: Sentence-transformers embeddings (
all-MiniLM-L6-v2) stored in ChromaDB via HTTP client for uncited claim search (backend/ingestion/embeddings.py). - Graph building: Resolve citations through CrossRef, enrich via Semantic Scholar, deduplicate repeated citations, and persist Paper/Author/Journal/Funder nodes and CITES edges to Neo4j (
backend/core/graph_builder.py). - Community & fraud analysis: Community detection and four statistical checks (GRIM, p-curve, small-sample, funding conflict) aggregated into
fraud_results(backend/core/community_detector.py,backend/core/fraud_detector.py). - Scoring & reporting: integrity score computation and Markdown audit report generation (
backend/core/integrity_scorer.py,backend/core/audit_reporter.py). - Privacy controls: signed-in users can delete their VeriScholar account data and configure a private OpenAI-compatible AI endpoint for future dashboard, REST, and MCP analyses.
Start services (recommended via Docker Compose):
docker compose up --buildOpen the app at http://localhost:3000, sign in with Google, create an API key in the dashboard, then upload from the browser or call the public REST API.
Public REST API example:
curl -X POST http://localhost:8000/v1/analyses \
-H "Authorization: Bearer vs_live_..." \
-F "file=@./paper.pdf"Then stream progress or fetch the result:
curl -N http://localhost:8000/v1/analyses/<analysis_id>/events \
-H "Authorization: Bearer vs_live_..."
curl http://localhost:8000/v1/analyses/<analysis_id> \
-H "Authorization: Bearer vs_live_..."MCP clients can use the same dashboard-created API keys with the Streamable HTTP endpoint:
# Local backend endpoint
http://localhost:8000/mcp/
# Production endpoint through Nginx
https://verischolar.knurdz.org/api/mcp/GET /health: service health and connectivity summary.GET /auth/google/start: starts Google OAuth sign-in.GET /auth/google/callback: Google OAuth callback.GET /auth/me: current session user.POST /auth/logout: revokes the current session.GET /dashboard/summary: usage, limits, and recent analyses for the signed-in user.GET /dashboard/api-keys: list dashboard-managed API keys.POST /dashboard/api-keys: create an API key. The secret is returned once.DELETE /dashboard/api-keys/{key_id}: revoke an API key.GET /config: effective LLM provider, model, and endpoint summary for the signed-in user. Requires login.GET /settings/ai: fetch custom AI settings metadata without returning the stored API key. Requires login.PUT /settings/ai: save a custom OpenAI-compatible endpoint, model name, and optional API key. Requires login.DELETE /settings/ai: clear custom AI settings and return to the system default provider. Requires login.DELETE /account: delete the signed-in user's sessions, API keys, AI settings, analyses, logs, and stored claim vectors. Requires login.GET /history: recent user-owned analysis summaries. Requires login.GET /analysis/{doc_id}: fetch a user-owned analysis. Requires login.GET /events/{doc_id}: Server-Sent Events stream for user-owned progress logs. Requires login.POST /analyze: upload a PDF or DOCX from the dashboard. Requires login.
All public v1 endpoints require Authorization: Bearer <api_key>.
POST /v1/analyses: upload a PDF or DOCX as multipart form data with afilefield; returns202withanalysis_id.GET /v1/analyses: list analyses owned by the API key's user.GET /v1/analyses/{analysis_id}: fetch status, result, score, and report.GET /v1/analyses/{analysis_id}/events: stream analysis progress logs with SSE.
REST base URLs:
# Production through Nginx
export VERISCHOLAR_API_BASE="https://verischolar.knurdz.org/api"
# Local backend direct
# export VERISCHOLAR_API_BASE="http://localhost:8000"
export VERISCHOLAR_API_KEY="vs_live_your_key"Submit a manuscript:
curl -X POST "$VERISCHOLAR_API_BASE/v1/analyses" \
-H "Authorization: Bearer $VERISCHOLAR_API_KEY" \
-H "Accept: application/json" \
-F "file=@/absolute/path/to/manuscript.pdf"Example response:
{
"status": "processing",
"doc_id": "f1b2c3d4e5...",
"analysis_id": "f1b2c3d4e5..."
}List analyses:
curl "$VERISCHOLAR_API_BASE/v1/analyses" \
-H "Authorization: Bearer $VERISCHOLAR_API_KEY"Fetch a result:
export ANALYSIS_ID="f1b2c3d4e5..."
curl "$VERISCHOLAR_API_BASE/v1/analyses/$ANALYSIS_ID" \
-H "Authorization: Bearer $VERISCHOLAR_API_KEY"Stream progress logs:
curl -N "$VERISCHOLAR_API_BASE/v1/analyses/$ANALYSIS_ID/events" \
-H "Authorization: Bearer $VERISCHOLAR_API_KEY" \
-H "Accept: text/event-stream"Testing with Postman, Insomnia, or Bruno:
- Create environment variables:
base_url,api_key, andanalysis_id. - Set
base_urltohttps://verischolar.knurdz.org/apiin production orhttp://localhost:8000locally. - Create
POST {{base_url}}/v1/analyses. - Set Authorization to Bearer Token and use
{{api_key}}, or addAuthorization: Bearer {{api_key}}. - In Body, choose
form-data. Add keyfile, change its type to File, and select a PDF or DOCX. - Send the request, then copy the returned
analysis_idinto the environment. - Poll
GET {{base_url}}/v1/analyses/{{analysis_id}}untilstatusisprocessedorfailed. - For live logs, call
GET {{base_url}}/v1/analyses/{{analysis_id}}/eventswithAccept: text/event-stream.
Common errors: 401 means the API key is missing, malformed, or revoked; 400 means the upload or payload is invalid; 413 means the file exceeds MAX_UPLOAD_BYTES; 429 means a rate limit was reached.
The MCP server is available at POST /mcp/ locally and /api/mcp/ through Nginx. Use the trailing slash in client configuration. It uses Streamable HTTP and requires Authorization: Bearer <api_key> on every request. Create API keys from the dashboard and store them outside shell history when possible.
MCP submissions run under the API key owner's account settings. If that user has configured a custom AI endpoint in Settings, MCP analysis jobs use that endpoint too.
Available tools:
verischolar_submit_analysis: submit a PDF/DOCX usingfilename,content_base64, and optionalmime_type.verischolar_list_analyses: list recent analyses for the API key's user.verischolar_get_analysis: fetch one analysis byanalysis_id.verischolar_get_analysis_events: fetch stored progress events from a zero-basedafter_index.verischolar_wait_for_analysis: wait briefly for new progress events or completion.
MCP smoke test:
python3 -m venv /tmp/verischolar-mcp-test
/tmp/verischolar-mcp-test/bin/pip install "mcp>=1.27,<2" "httpx==0.27.2"
export VERISCHOLAR_MCP_URL="https://verischolar.knurdz.org/api/mcp/"
export VERISCHOLAR_API_KEY="vs_live_..."
/tmp/verischolar-mcp-test/bin/python - <<'PY'
import asyncio
import os
import httpx
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
async def main():
async with httpx.AsyncClient(
headers={"Authorization": f"Bearer {os.environ['VERISCHOLAR_API_KEY']}"},
timeout=30.0,
) as http_client:
async with streamable_http_client(os.environ["VERISCHOLAR_MCP_URL"], http_client=http_client) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("\\n".join(sorted(tool.name for tool in tools.tools)))
asyncio.run(main())
PY
rm -rf /tmp/verischolar-mcp-testIf a key is pasted into logs, chat, or shared terminals, revoke it from the dashboard and create a fresh one.
Use this production endpoint for remote clients:
https://verischolar.knurdz.org/api/mcp/
Cursor can use .cursor/mcp.json in a project or ~/.cursor/mcp.json globally:
{
"mcpServers": {
"verischolar": {
"url": "https://verischolar.knurdz.org/api/mcp/",
"headers": {
"Authorization": "Bearer ${env:VERISCHOLAR_API_KEY}"
}
}
}
}VS Code can use .vscode/mcp.json in a workspace:
{
"inputs": [
{
"id": "verischolar-api-key",
"type": "promptString",
"description": "VeriScholar API key",
"password": true
}
],
"servers": {
"verischolar": {
"type": "http",
"url": "https://verischolar.knurdz.org/api/mcp/",
"headers": {
"Authorization": "Bearer ${input:verischolar-api-key}"
}
}
}
}Claude Code can add VeriScholar as a remote HTTP MCP server:
export VERISCHOLAR_API_KEY="vs_live_your_new_key"
claude mcp add --transport http verischolar \
https://verischolar.knurdz.org/api/mcp/ \
--header "Authorization: Bearer $VERISCHOLAR_API_KEY"
claude mcp listInside Claude Code, run /mcp to inspect connected servers, then ask Claude to list VeriScholar analyses or submit a document.
Codex can add the Streamable HTTP server from the CLI:
export VERISCHOLAR_API_KEY="vs_live_your_new_key"
codex mcp add verischolar \
--url https://verischolar.knurdz.org/api/mcp/ \
--bearer-token-env-var VERISCHOLAR_API_KEYOr configure Codex directly in ~/.codex/config.toml:
[mcp_servers.verischolar]
url = "https://verischolar.knurdz.org/api/mcp/"
bearer_token_env_var = "VERISCHOLAR_API_KEY"Rate-limit headers are returned on limited endpoints:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-Afteron429
Default public-beta limits are 5 analysis submissions per day, 2 per hour, 1 active analysis per user, 120 result/status reads per minute, and 5 active API keys per user.
When the frontend is served through Nginx, public API calls are routed through /api/* and proxied to the backend. Direct local public REST calls use the backend port directly, such as http://localhost:8000/v1/analyses.
doc_id,pages_parsed,claims_found,citations_found,reference_count,citation_mentions_foundintegrity_score,integrity_verdict,score_breakdownfraud_riskand sub-results (grim,p_curve,sample_size_flags,funding_conflicts)citations_resolved,cartel_risk,suspicious_clusters,retracted_papersaudit_report(Markdown string)
- Backend values are loaded from
.envthroughbackend/core/config.py. - Signed-in users can override the system LLM route for their own future analyses from the Settings page. Custom AI settings are stored in SQLite, scoped to the user, and the API key is never returned by browser endpoints after save.
- Database and vector store:
NEO4J_URI(defaultbolt://neo4j:7687)NEO4J_USER(defaultneo4j)NEO4J_PASSWORD(defaultverischolar_secret)CHROMA_HOST(defaultchromadb)CHROMA_PORT(default8000)
- LLM provider:
LLM_PROVIDER(ollamaoropenai, defaultollama)OLLAMA_HOST(defaulthttp://ollama:11434)OLLAMA_MODEL(defaultmistral)OPENAI_API_KEYOPENAI_ENDPOINT(defaulthttps://api.openai.com)OPENAI_MODEL(defaultgpt-4o-mini)
- Pipeline tuning:
CLAIM_PAGE_CONCURRENCY(default3): concurrent per-page LLM claim extraction workers. Lower this for local Ollama if the machine is resource constrained.CLAIM_PAGE_TIMEOUT(default180): timeout in seconds for each page-level claim extraction LLM call.CITATION_CROSSREF_CONCURRENCY(default8): concurrent CrossRef resolution requests.CITATION_SEMANTIC_SCHOLAR_CONCURRENCY(default4): concurrent Semantic Scholar enrichment requests.CITATION_REQUEST_TIMEOUT(default12): timeout in seconds for citation metadata requests.
- Runtime:
FASTAPI_ENV(defaultdevelopment)
- Public app/auth:
FRONTEND_BASE_URL(defaulthttps://verischolar.knurdz.org)ALLOWED_CORS_ORIGINSALLOWED_CSRF_ORIGINSAPP_DB_PATH(default/data/verischolar.sqlite3)MCP_SERVER_URL(defaulthttps://verischolar.knurdz.org/api/mcp/)MCP_ALLOWED_HOSTS(defaultverischolar.knurdz.org,localhost:8000,127.0.0.1:8000)MAX_UPLOAD_BYTES(default52428800)GOOGLE_OAUTH_CLIENT_IDGOOGLE_OAUTH_CLIENT_SECRETGOOGLE_OAUTH_REDIRECT_URISESSION_SECRETSESSION_COOKIE_SECURE(truein production)API_KEY_PEPPERENABLE_API_DOCS(falsein production by default)
- Public-beta limits:
RATE_LIMIT_ANALYSIS_PER_DAYRATE_LIMIT_ANALYSIS_PER_HOURRATE_LIMIT_READS_PER_MINUTERATE_LIMIT_ACTIVE_ANALYSES_PER_USERMAX_ACTIVE_API_KEYS_PER_USER
The Settings page includes two privacy-oriented controls:
- Custom AI model: users can provide an OpenAI-compatible endpoint, API key, and model name. New dashboard uploads, REST API submissions, and MCP submissions for that user use the custom model route for LLM extraction steps.
- Delete account data: users can remove their account sessions, dashboard-created API keys, custom AI settings, analysis rows, progress logs, and ChromaDB claim vectors. Deletion is blocked while an analysis is active so an in-flight job does not continue processing a manuscript after the removal request.
Generate SESSION_SECRET and API_KEY_PEPPER with:
openssl rand -base64 32Create or select a Google Cloud project, configure the OAuth consent screen, then create an OAuth 2.0 Client ID with application type Web application.
Authorized redirect URIs:
- Production:
https://verischolar.knurdz.org/api/auth/google/callback - Local via Next proxy:
http://localhost:3000/api/auth/google/callback
For local development with the localhost 3000 callback, run the backend on 8000 and set the frontend server env:
API_PROXY_TARGET=http://localhost:8000
NEXT_PUBLIC_API_URL=/apiCopy the generated Google Client ID and Client Secret into .env as GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET.
Frontend configuration lives in frontend/.env.local for local development:
NEXT_PUBLIC_API_URL=/api
API_PROXY_TARGET=http://localhost:8000In production, the frontend defaults to relative /api routes so Nginx can proxy requests to the backend.
Frontend development commands:
cd frontend
npm install
npm run dev
npm run build
npm run start
npm run lintImportant frontend paths:
frontend/src/app/page.js: public landing page.frontend/src/app/login/page.js: Google sign-in screen.frontend/src/app/dashboard/page.js: API key and usage dashboard.frontend/src/components/AuthGate.js: client-side session guard for protected pages.frontend/src/app/analyze/[id]/page.js: live Server-Sent Events progress screen.frontend/src/app/audit/page.js: protected upload workspace.frontend/src/app/report/[id]/page.js: audit report view.frontend/src/app/settings/page.js: backend configuration summary, custom AI model settings, and account-data deletion.frontend/src/app/docs/page.js: product and API documentation page.frontend/src/app/privacy/page.js: privacy policy and user data controls.frontend/src/lib/api.js: API helper functions andNEXT_PUBLIC_API_URLhandling.
Deployment helper:
./deploy.shThe deployment helper checks for Docker and Docker Compose, creates .env interactively when one is missing, starts the Ollama profile when LLM_PROVIDER=ollama, and runs docker compose up -d --build.
- Python 3.10+
- FastAPI (HTTP API)
- PyMuPDF (
fitz) and python-docx (parsing) - chromadb (ChromaDB HTTP client)
- sentence-transformers (
all-MiniLM-L6-v2) for embeddings - neo4j Python driver for graph storage
- scipy for statistical tests
- httpx for HTTP calls to Ollama and other services
- DOCX documents are chunked into pseudo-pages (character-based) which can affect proximity-based heuristics.
- Claim extraction depends on the configured Ollama model and prompt; accuracy varies with model choice and prompt tuning.
- Citation resolution prefers DOI matches; fuzzy CrossRef searches may produce imperfect matches.
- Bibliography/reference extraction prefers explicit
References,Bibliography,Literature Cited, orWorks Citedsections and falls back from numbered references to author-year parsing. - Increasing LLM or citation concurrency can reduce wall-clock time, but very high values may hit model, CPU/GPU, or public API rate limits.
- Fraud detection is statistical/heuristic and intended to flag items for manual review, not to produce definitive legal conclusions.
- Backend entry:
backend/main.py - Pipeline orchestration:
backend/core/langgraph_app.py - Parsers & detectors:
backend/ingestion/(parser.py,citation_detector.py,claim_classifier.py,embeddings.py) - Graph & enrichment:
backend/core/graph_builder.py,backend/core/crossref_client.py,backend/core/semantic_scholar_client.py
- Extract exact dependency versions from
backend/requirements.txtand add them here. - Add example API request/response snippets and an example audit report.
- Create a
LICENSEfile (MIT) and a minimal GitHub Actions CI workflow.
If you want the README adjusted further to emphasize any changed files or new behavior, tell me which areas to expand.