Back to cookbook
EZ

Eric Zakariasson

Added ago

TypeScript ยท Intermediate

๐• Post to Fact-Check

View as Markdown

This app builds an AI fact-checker with the Grok API that checks the claims in a post on ๐• against the web and ๐•. It's a pattern for research that has to cite its sources, with each claim checked in parallel.

Paste a link to a post on ๐•. Grok reads it, images and video included, and splits it into claims. Then it checks every claim on the web and on ๐• in its own request, all at the same time, and writes a note like a Community Note, where every sentence links to sources the searches actually returned. It runs as a small web app or in your terminal, and shows each search and the pages it finds as they come in.

What you'll learn

  • Find a post from its link with X Search, and read its images and video with enable_image_understanding and enable_video_understanding
  • Pull a post's claims out as structured output, and show them while Grok writes them
  • Fan out: research each claim in its own request with webSearch() and xSearch(), all at once, then merge the results
  • Trust citations instead of text: keep only the sources that came back as citations, and answer "not enough evidence" when no source backs a verdict
  • Write a note that can only cite the sources you kept, with an enum of their numbers in the schema
  • Show each search and the pages it found as it happens, with server_tool_call events and include: ["web_search_call.action.sources"]
  • Switch the research to grok-4.20-multi-agent, where four agents work on each claim
  • Stream progress from a Node server to a web page with server-sent events, and cancel the work when the page closes

Run it

You need Node.js 22.13 or later. Put your API key in .env at the root of the repo, or export XAI_API_KEY.

Bash

cd examples/x-post-fact-check/typescript
npm install
npm run web

Open http://localhost:3000, paste a link to a post and click Fact-check, or click one of the four sample posts: insider buying at GameStop (with an image), Starship's first orbital flight (with a video), Harry Kane drawing level with England's caps record, and this year's Nobel Prize in Physics. The page works like a fact-checking desk. The post fills in on the left as Grok reads it, with the image or video Grok looked at and what it saw there. Each claim gets a card on the right that fills with the searches Grok runs and the sites they find, then gets a verdict: supported, missing context, or not enough evidence. The note appears under the post as Grok writes it, with its sources numbered underneath, and the page shows what the run cost. Turn on Deep research to have four agents research each claim. Click Stop or close the page to cancel.

To check a post from the terminal instead, pass its link, and add --deep for deep research. It defaults to the GameStop post.

Bash

npm start -- https://x.com/unusual_whales/status/2106776609457844610

That saves the note, the claims, and their sources to output/ as Markdown, with the whole result as JSON next to it.

When we ran it, a check took 39 seconds for the Starship post, and about three minutes for the GameStop post, which has twice as many claims and ran while the API was slow. A check of the Starship post cost about 15 cents and one of the GameStop post 30 to 40 cents. About a third of that is for searches: X Search is billed at $5 per 1,000 posts it fetches and web search at $5 per 1,000 searches. The rest is for 50,000 to 130,000 tokens. At the end, the page and the terminal show what the API reported the run cost.

How it works

The shared code is in src/factcheck.ts. factCheck() runs three steps and reports each one through callbacks, so the web app and the terminal app show the same progress in their own way:

  1. readPost() streams a grok-4.7 request with xSearch(), and asks for the post's text, what its images and video show, and up to four claims as JSON. Grok fetches the post with X Search's x_thread_fetch, looks at its images by opening them on X's image server, and watches its video with view_x_video. The server_tool_call event reports each of those as it happens, with the image's or video's URL, which the page uses to show them. The json event hands over the post and its claims as Grok writes them, so each claim gets its card right away. Grok cites the post only if X Search returned it, so a link whose post doesn't come back as a citation fails instead of letting Grok guess what it said. The post's date comes from its ID, which holds the time it was published.
  2. checkClaim() researches one claim in its own streamed request with webSearch() and xSearch(), and factCheck() runs one for every claim at the same time. Grok returns the sources it relied on, a verdict, and why, as JSON. The request sets include: ["web_search_call.action.sources"], so each web search arrives in a server_tool_call event with the pages it found. The citation event lists every page and post the searches returned. A source Grok lists that isn't among them is dropped, and a verdict only stands if a kept source backs it: supported needs a source that supports the claim, and missing context needs one that contradicts it or adds context. Anything else becomes not enough evidence. A claim whose request fails becomes not enough evidence too, and the other claims carry on.
  3. writeNote() numbers the kept sources of every claim and asks for the note as JSON: a list of sentences, each with the numbers of the sources it comes from. In the schema, those numbers are an enum of the kept sources, and minItems: 1 makes every sentence cite one, so the note can't cite anything the searches didn't return. Claims without a kept source are left out of it. The json event hands over each sentence as Grok writes it.

All three steps use a low reasoning effort. Even so, Grok ran two to four searches for each claim, and in our runs it rated the GameStop post's claim that its CEO bought "about $57 million" as missing context: the filings, and the post's own image, add up to about $74 million.

With Deep research on, or --deep in the terminal, checkClaim() uses grok-4.20-multi-agent instead. For this model, reasoning.effort sets how many agents work on each claim rather than how long it thinks: low or medium is four agents, and high or xhigh is 16. The app uses low. Only the leading agent's tool calls come back, so a card shows fewer searches than the agents ran. The multi-agent model takes built-in tools like webSearch() and xSearch() but not your own functions, which is all this step needs. Deep research stores its responses for 30 days, which is the API's default. When a response isn't stored, the SDK asks for encrypted reasoning, and the multi-agent model then sends back every agent's encrypted state, which can make one stream event bigger than the 1 MiB the SDK reads.

src/server.ts runs factCheck() for the link from the page and sends each step to it as a server-sent event. It passes an AbortSignal to every request, so closing the page or clicking Stop cancels whatever is running. public/index.html is plain HTML and JavaScript that shows those events. It loads the post's image or video straight from X's servers. src/index.ts prints the same steps in the terminal and saves the result to output/.

More from the cookbook