GET /api/postcards?url=... every 3–5 seconds to check progress.
Pipeline stages
The stages are defined insrc/lib/config.ts as PIPELINE_STAGES. Each stage carries a key, a user-facing message, and a progress value from 0 to 1.
starting — 0%
Initializing postcard. A new row is inserted into the
postcards table with status: "processing" and stage: "starting". The pipeline ID is returned to the caller immediately.scraping — 10%
Fetching post content from the platform. The
UnifiedPostStrategy inspects the URL, selects the appropriate ingestion client, and retrieves the post’s markdown, author, and timestamp. If the primary client fails, the system falls back to Jina Reader.scraped — 30%
Content fetched successfully. The pipeline validates the scraped content: checks minimum length (50 characters), detects Cloudflare challenges, and flags login walls. If validation fails, the pipeline sets
verdict: "insufficient_data" and halts without proceeding to corroboration.corroborating — 40%
Searching for primary sources with Gemini. The corroboration agent uses Gemini’s
google_search tool to find independent sources from trusted domains that support or refute the post’s claims. Up to POSTCARD_MAX_TOOL_CALLS (default: 5) search queries are executed, returning up to POSTCARD_MAX_SOURCES (default: 10) sources.auditing — 70%
Verifying origin and temporal alignment. The verifier agent checks whether the URL is reachable, whether the hostname matches the declared platform, and whether the post’s timestamp is consistent with search results.
scoring — 90%
Calculating the Postcard score. The four subscores are combined with their configured weights. The result is floored to an integer (0–100) and written to the
postcards table alongside the verdict and all source data.src/lib/config.ts:
The heartbeat pulse system
Long-running stages (corroborating, auditing) can take 10–30 seconds. To reassure users that the system is still working, each slow stage activates a heartbeat pulse viacreatePulse() in src/lib/postcard.ts.
The pulse fires every 3 seconds and cycles through rotating sub-messages:
"", ".", "..", "...") and written back to the postcards table so polling clients always see a fresh message. The pulse is stopped as soon as the stage completes.
Async background processing
The pipeline runs entirely server-side. The POST endpoint returns HTTP 202 immediately after creating thepostcards row. The actual pipeline runs in a detached async context.
Cache check before running
Before the pipeline starts, the system checks for an existing completed result at the normalized URL:refresh is not set, the cached result is returned immediately without running the pipeline. See Caching for full details.
Concurrent request deduplication
If a pipeline is already"processing" for a given URL, new requests for that same URL poll the existing pipeline rather than starting a second one. This prevents duplicate Gemini API calls for the same content.
Status lifecycle
A postcard row moves through these status values over its lifetime:| Status | Meaning |
|---|---|
pending | Row created, pipeline not yet started |
processing | Pipeline is actively running |
completed | Pipeline finished; forensic report available |
failed | An unrecoverable error occurred |
Failure handling
The pipeline setsstatus: "failed" when it encounters:
- Login wall — scraped markdown contains
"login"or"sign in" - Cloudflare block — markdown contains
"Checking if the site connection is secure" - Content too short — markdown is fewer than 50 characters
- Platform not recognized —
platformresolves to"Other"after scraping - API errors — Gemini rate limits (HTTP 429) or network failures
error column is populated with the error message.
Fake mode
SetNEXT_PUBLIC_FAKE_PIPELINE=true to replace the live pipeline with mock data. Useful for demos or development when you want to avoid hitting the Gemini API quota.
PIPELINE_STAGES with the configured delay, but returns a hardcoded PostcardResponse with a postcardScore of 0.85 and a "verified" verdict. Any URL containing the string "fail" will trigger a fake failure regardless of NEXT_PUBLIC_FAKE_PIPELINE_FAIL.
Fake mode is controlled by a
NEXT_PUBLIC_ variable, which means it is embedded into the client bundle at build time. You must rebuild the application after changing it.