Documentation

TruthCheck Docs

Everything you need to understand, run, and extend TruthCheck v7.0.

What is TruthCheck?

TruthCheck is an AI-powered bilingual fact-checking application. It verifies claims against global evidence using DeepSeek Reasoner and real-time web search (Brave Search primary, DuckDuckGo fallback) — available in English and Spanish.

  • ▸Bilingual UI (EN/ES) — full interface and AI output in both languages
  • ▸AI Claim Extraction — identifies atomic, verifiable statements from any text
  • ▸Parallel Verification — all claims verified simultaneously for minimum wait time
  • ▸Progressive Results — cards appear incrementally as each verification completes
  • ▸Structured Verdicts — one of seven verdicts (Supported through Contradicted), each with a confidence level and an assessment of how good the evidence behind it was
  • ▸Stated Caveats — scope, timing, definition, and correlation-vs-causation limits are called out alongside the verdict rather than left implicit
  • ▸Legacy 0–5 Score — still returned, derived from the verdict for older clients
  • ▸Verbatim Quotations — the quoted passage is always an exact substring of the text you submitted, never a translation or paraphrase
  • ▸Errors Are Not Verdicts — a search outage returns an explicit error, never "Unverified"
  • ▸Grounded Citations — every factual statement in the explanation links to the exact passage it came from, which you can expand and open
  • ▸Source Provenance — sources whose page was actually read are marked as such; a search-result summary is never presented as verified content
  • ▸Source Evidence — up to 4 web search results per claim as clickable source cards
  • ▸Real-time Status — progress bar, pending claim feed, elapsed timer, live domain feed
  • ▸Copy Result — one-click clipboard export of claim, verdict, reasoning, and sources
  • ▸Character Counter — amber/red threshold warnings at 12k/15k chars
  • ▸Empty-State Handling — an explicit "no verifiable claims found" message when extraction succeeds with zero claims
  • ▸Keyboard Shortcut — Ctrl+Enter / ⌘+Enter to submit
  • ▸Dark Mode — follows system preference automatically
  • ▸Error Monitoring — Sentry integration for server-side exceptions

Quick Start

Prerequisites

  • Node.js 20+
  • DeepSeek API key — platform.deepseek.com
  • Brave Search API key (optional — required on Vercel/cloud where DuckDuckGo is blocked)

Setup

git clone <repo-url>
cd truth-checker
npm install

# Copy and fill in environment variables
cp .env.example .env.local
# Required: DEEPSEEK_API_KEY
# Optional: BRAVE_SEARCH_API_KEY
# Optional: FACTCHECK_* pipeline budgets (see docs/DEVELOPMENT.md)

npm run dev

App available at http://localhost:3000

Common Commands

npm run dev          # Start dev server
npm run build        # Production build
npm run start        # Start production server
npm run lint         # ESLint
npm run typecheck    # tsc --noEmit
npm test             # Run all tests (Vitest)
npm run test:watch   # Watch mode

Tech Stack

LayerTechnology
FrameworkNext.js 16.2.3 (App Router, standalone)
AI — ExtractionDeepSeek Chat (deepseek-chat)
AI — ReasoningDeepSeek Reasoner (deepseek-reasoner)
SearchBrave Search API (primary) / DuckDuckGo HTML scraper (fallback)
ValidationZod — LLM response schema enforcement
Error MonitoringSentry (@sentry/nextjs)
i18nCustom lib/i18n.ts + React Context
AnalyticsVercel Analytics
TestingVitest + React Testing Library
StylingTailwind CSS v4
IconsLucide React
Reverse Proxynginx 1.27 (TLS, rate limiting)
ContainerizationDocker / Docker Compose

Security

ControlDetails
Rate LimitingPer-IP sliding window: 5 req/min (/analyze), 10 req/min (/extract), 20 req/min (/verify)
Security HeadersCSP, HSTS (1 year + preload), X-Frame-Options: DENY, X-Content-Type-Options, Permissions-Policy
Input Validation15,000 char limit; max 10 claims per extraction; claim.id format enforced; empty input rejected
SecretsHard startup failure if DEEPSEEK_API_KEY is missing — no mock/fallback
Network IsolationNext.js on internal Docker network only; nginx is the sole public entry point
TLSMozilla Intermediate config — TLS 1.2/1.3, strong ciphers, OCSP stapling
Audit LoggingStructured JSON logs on every API request (timestamp, IP, route, payload size, duration, status)
Prompt Injection HardeningFixed fact-checking policy sent as a system message; claim + evidence confined to a delimited, length-bounded user message
Proxy-Safe Client IPRate limiting and audit logs key on a deployment-specific trusted header (X-Real-IP behind nginx, X-Forwarded-For on Vercel) — never a spoofable client-supplied value

Architecture

TruthCheck is built on Next.js 16 App Router. All AI logic runs server-side in API routes. The frontend is a single client component (app/page.tsx) that orchestrates the full pipeline.

Analysis Pipeline

  1. User submits text → POST /api/extract → DeepSeek Chat extracts atomic claims (max 10)
  2. Frontend fans out parallel POST /api/verify calls — one per claim
  3. Each verify call plans a bounded set of search queries, runs them against Brave Search (DuckDuckGo fallback), and deduplicates the candidates
  4. The DeepSeek Reasoner is called only when evidence was actually obtained — every other outcome short-circuits with an explicit status
  5. Results stream in one-by-one; each replaces its skeleton card as it arrives

Errors Are Not Verdicts

Every result carries a pipelineStatus that separates infrastructure failure from a genuine absence of evidence. If the search providers are unreachable, the claim comes back as an error, not as “Unverified” — a service being down is not a finding about the claim. The same holds when a check runs out of time: each claim has a hard deadline, and exceeding it is reported as its own timeout status and labeled Time limit exceeded, not “System error” or doubt about the claim.

Key Files

app/page.tsxRoot client component — UI layout and prop wiring
hooks/useAnalyze.tshandleAnalyze() — pipeline state, fetch orchestration, error handling
app/api/extract/route.tsPOST /api/extract — claim extraction via deepseek-chat
app/api/verify/route.tsPOST /api/verify — evidence search + reasoning via deepseek-reasoner
lib/analyzer.tsextractClaims() + verifyClaim() — extraction and pipeline orchestration
lib/factcheck/types.tsPipeline contracts — dependency-free, safe to import from client code
lib/factcheck/quote.tsresolveQuote() — guarantees a quotation is verbatim from the input
lib/factcheck/searchPlan.tsBounded query planning with a deterministic single-query fallback
lib/factcheck/search.tsProvider orchestration → typed SearchOutcome (Brave primary, DuckDuckGo fallback)
lib/factcheck/urlSafety.tsAddress and URL eligibility — the retrieval security boundary
lib/factcheck/retrieve.tsBounded page fetch with DNS pinning and redirect re-validation
lib/factcheck/passages.tsSentence chunking and relevance ranking of page text
lib/factcheck/verify.tsCitation prompt, server-side validation, single repair retry
lib/factcheck/outcome.tsThe status → score table: operational failure vs. no evidence
lib/factcheck/config.tsFACTCHECK_* budgets, validated and clamped at startup
lib/i18n.tsTranslation dictionary (en/es) + dynamic string helpers
lib/i18nContext.tsxLanguageProvider + useLanguage() hook

Development Guide

Adding a Translation Key

  1. Add the English string to dict.en in lib/i18n.ts
  2. Add the Spanish string to dict.es with the same key
  3. Use it in any client component: const { t } = useLanguage(); t('my_key')

The test suite automatically enforces that dict.en and dict.es have identical keys.

Styling

Tailwind CSS v4. Custom utility classes in app/globals.css:

  • .glass — glassmorphism with backdrop blur
  • .gradient-text — indigo-to-cyan gradient text
  • .hero-pattern — radial dot grid background

Docker

docker compose up -d --build   # Build and start
docker compose down             # Stop

API Reference

POST/api/extract

Extracts atomic factual claims from a body of text using deepseek-chat. Returns at most 10 claims.

// Request
{ "text": "...", "language": "en" }

// Response — originalTextQuote is always verbatim from the input, never translated
{ "claims": [{ "id": "claim-0", "text": "...", "originalTextQuote": "...",
               "quoteStart": 0, "quoteEnd": 45 }] }

POST/api/verify

Verifies a single claim via web search + DeepSeek Reasoner. Pass "language": "es" to receive reasoning in Spanish.

// Request
{ "claim": { "id": "claim-0", "text": "...", "originalTextQuote": "..." }, "language": "en" }

// Response — read "verdict", not "legacyScore"
{ "result": { "claimId": "claim-0", "score": 0, "reasoning": "...",
              "sources": [...], "citations": [], "pipelineStatus": "ok",
              "phase2": {
                "verdict": "contradicted",
                "confidence": { "level": "high", "score": 0.91, "rationale": "..." },
                "evidenceQuality": { "level": "high", "rationale": "...", "limitations": [] },
                "summary": "...", "caveats": [],
                "supportingEvidence": [], "contradictingEvidence": [...],
                "legacyScore": 0 } } }

// Response when the search providers are down — an error, not a verdict
{ "result": { "claimId": "claim-0", "score": -1, "reasoning": "...",
              "sources": [], "citations": [],
              "pipelineStatus": "search_unavailable",
              "warnings": [{ "code": "search_provider_failed" }] } }

Pipeline Status

StatusScoreMeaning
ok0–5Evidence was obtained and judged
no_results1A provider answered successfully but returned nothing
insufficient_evidence1Sources were found but none was relevant enough
search_unavailable-1Every search provider failed
retrieval_unavailable-1Candidates existed but every page fetch failed

Verdicts

The primary result. A verdict describes what the evidence showed — it is never a statement that the claim is true or false in the abstract.

VerdictMeaning
supportedThe retrieved evidence directly supports the claim
mostly_supportedSupported in substance, with a caveat that does not overturn it
mixedCredible evidence points both ways, or the claim is true only under some readings
insufficient_evidenceNot enough relevant evidence was retrieved to judge it either way
mostly_contradictedContradicted in substance, with some support that survives
contradictedThe retrieved evidence directly contradicts the claim
not_checkableOpinion, prediction, or otherwise not verifiable against evidence

Confidence and evidence quality

Confidence (high / medium / low) is how firmly the verdict follows from the evidence. Evidence quality (high / medium / low / insufficient) is how good that evidence was — its independence, primacy, and directness. They are separate on purpose: strong agreement among weak sources is not the same as a firm conclusion. A directional verdict can never be reported as high confidence on low or insufficient evidence; that cap is enforced by the server, not by the model.

Caveats

A verdict can carry caveats naming what qualifies it — missing context, time sensitivity, geographic or population scope, ambiguous definitions, correlation reported as causation, units and quantities, conflicting sources, or thin primary evidence. Caveats marked materialare ones that shaped the verdict itself. Caveat detection is the weakest part of this pipeline today and misses a meaningful share of what it should catch — read the absence of a caveat as "not found", not as "not applicable".

Legacy Score Interpretation

The 0–5 score predates verdicts and is now derived from them for backward compatibility. It is lossy — insufficient_evidence and not_checkable both collapse to 1. New clients should read verdict.

ScoreLabelMeaning
-1ErrorVerification could not run (search or API failure)
0ContradictedEvidence explicitly contradicts the claim
1UnverifiedNo relevant evidence found
2–3UncertainEvidence is partial, ambiguous, or inconclusive
4–5Likely TrueMultiple sources explicitly confirm the claim