Inspiration

It started in a group chat. Someone posted a news article and immediately half the conversation turned into "that outlet is biased" vs "no, that's literally what happened." Nobody could agree on whether the source was trustworthy, let alone the actual story.

What bugged us wasn't the argument — it was that there was no good way to map it out. You had to hold every contradiction in your head at once, which is exhausting and easy to mess up. We thought: what if you could just see it? Lay every claim side by side, show who agrees, who doesn't, and where the story starts falling apart. That's where Tacitus came from.

What It Does

Tacitus is an investigative research tool for tracking claims across multiple sources and automatically flagging where they conflict. You add sources — outlets, people, documents — attach them to specific claims, and tag whether each source agrees or disagrees. The app then classifies every claim into one of four buckets in real time:

  • Conflict — at least one source agrees and one disagrees
  • Confirmed — two or more independent sources agree
  • Gap — only one source has weighed in
  • Unverified — nobody has said anything yet

The classifier is just clean math. If \( A \) is agreeing sources and \( D \) is disagreeing:

$$ \text{type} = \begin{cases} \text{Conflict} & A \geq 1 \text{ and } D \geq 1 \ \text{Confirmed} & A \geq 2 \text{ and } D = 0 \ \text{Gap} & A = 1 \text{ and } D = 0 \ \text{Unverified} & \text{otherwise} \end{cases} $$

We also built a Fact Check tab where you enter an author's name and it runs a live web search through the Gemini API, returning a reliability score from \( 0 \) to \( 100 \), a political leaning label, known contradictions, and notes on source freshness.

How We Built It

Backend is Node.js + Express. We used MongoDB with Mongoose for storing users and sessions, and Pug + Sass on the frontend. Auth runs through Passport — local email/password plus passkey support via @simplewebauthn, which felt like a win to actually ship.

The most interesting piece was the Fact Check controller. We call the Gemini 2.5 Flash API with Google Search grounding enabled, feed it a structured prompt asking for JSON, then strip markdown fences and parse:

const body = {
  contents: [{ parts: [{ text: buildPrompt(author, focus, timeframe) }] }],
  tools: [{ google_search: {} }],
};

Challenges

Prompt engineering was genuinely hard. We went through five versions of the system prompt before it reliably returned clean JSON. The model kept sneaking in a sentence of explanation before the {, which immediately broke JSON.parse. We ended up writing an extractJsonString helper that strips fences and finds the first { to the last } as a fallback.

Scope creep in reverse. We had a ton of ideas — timeline view, evidence vault, PDF attachments. Cutting them mid-hackathon while everyone's still hyped is genuinely hard. Scope management is a real skill and we learned that the hard way.

MongoDB session headaches. Getting connect-mongo playing nicely with express-session ate about an hour. Turned out to be a version mismatch we completely overlooked. Classic.

What We Learned

The biggest takeaway: deterministic logic beats asking an LLM to make structural decisions. The claim classifier is just math — it doesn't hallucinate, doesn't drift, always gives the same answer for the same inputs. AI is great for interpretation. It should not be the thing deciding whether a claim is confirmed. That was worth learning early.

Also: every LLM output pipeline needs a fallback parser, no exceptions. And knowing what to cut from a demo matters just as much as knowing what to build.

What's Next

  • Evidence vault — attach PDFs and source links directly to claims
  • Multi-user collaboration with editor/viewer roles
  • Timeline mode to visualize how a narrative drifts over time
  • One-click PDF export so you can hand someone an actual brief

Built With

Share this project:

Updates