FACTCHECK ========= Video-audio transcript and article fact-checking API powered by OpenAI models, local yt-dlp audio downloads, Cohere transcriptions, and Exa search evidence. Routes | Method | Path | Description | | ------ | --------------- | ----------------------------------- | | GET | /api/health | service status and configuration | | GET | / | this plain-text API helper page | | POST | /api/check | run a fact-check (direct or queued) | | GET | /api/check/:id | poll a queued job | POST /api/check — video URL audio transcription Non-webpage video URLs are processed as audio only: the API downloads the audio locally with yt-dlp, sends it to Cohere for transcription, then uses OpenAI and Exa evidence to produce the fact-check. Raw video ingestion, external downloader services, and video file uploads are not supported. curl -X POST http://localhost:7110/api/check \ -H 'content-type: application/json' \ -d '{ "url": "https://www.tiktok.com/@user/video/123", "sourceType": "video", "searchType": "deep", "effort": "medium", "mode": "direct" }' POST /api/check — article/webpage URL Article fact-checking is unchanged: set `sourceType: "webpage"` when the URL should be treated as an article or webpage instead of video audio. curl -X POST http://localhost:7110/api/check \ -H 'content-type: application/json' \ -d '{ "url": "https://example.com/article.html", "sourceType": "webpage", "searchType": "instant", "mode": "direct" }' Queue polling curl http://localhost:7110/api/check/12345678 Request fields | Field | Type | Default | Description | | ----------------- | ------------------ | ------- | ---------------------------------------------------- | | url | string | — | required absolute http(s) URL | | sourceType | string | auto | auto, video, webpage | | searchType | string | auto | instant, deep, reasoning (or raw Exa values) | | model | string or string[] | env | OpenAI model ID, or `[searchPlan, finalAnswer]` | | effort | string | env | OpenAI reasoning effort | | mode | string | direct | direct (wait) or queue (return job id) | | additionalContext | string | — | optional extra instructions for the prompt | | iosCompatible | boolean | true | passed to downloader | | proxy | boolean | true | use `PROXY_URL` for video audio downloads when configured; forced for YouTube/Instagram | Multipart/form-data video uploads are rejected. Send a JSON request with the video URL instead so the API can download and transcribe the audio. Proxy behavior: video audio downloads proxy by default when `PROXY_URL` is configured. Set `"proxy": false` to explicitly opt out for services where direct downloads are allowed. YouTube and Instagram are always force-proxied and require `PROXY_URL`; they cannot opt out. Supported OpenAI models are `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.2`, `gpt-5`, `gpt-5-mini`, and `gpt-5-nano`. searchType values | Alias | Exa value | Description | | --------- | --------------- | ----------------------------------- | | instant | instant | lowest latency, real-time optimized | | deep | deep | light deep search | | reasoning | deep-reasoning | base deep search | Also accepts raw Exa values directly: auto, neural, fast, deep-lite, deep, deep-reasoning, instant. Response fields | Field | Description | | ------------- | ----------------------------------------------------- | | id | request or job id | | inputMode | always url | | url | source URL | | provider | openai | | model | model string or two-model array used | | models | { searchPlan, finalAnswer } | | effort | OpenAI reasoning effort | | analysis | plain-text fact-check with Confidence: X/10 | | reasoning | null | | download | null | | uploadedFile | null | | transcription | Cohere transcript metadata and audio metadata for video URLs | | webpage | Exa article metadata for article mode | | research | Exa queries, searchType used, and full-text results | | usage | prompt, output, reasoning, and total tokens | | warnings | array of informative warnings | Environment | Variable | Required | Description | Default | | ------------------------------ | -------- | ---------------------------------------------------- | ----------------------- | | OPENAI_API_KEY | yes | OpenAI API key | — | | EXA_API_KEY | yes | Exa API key | — | | COHERE_API_KEY | yes* | Cohere API key for video audio transcription | — | | OPENAI_MODEL | no | default OpenAI model | gpt-5.6-sol | | OPENAI_EFFORT | no | default OpenAI effort | medium | | EXA_SEARCH_TYPE | no | default searchType | auto | | PORT | no | port the API listens on | 7110 | | PROXY_URL | yes* | proxy URL for yt-dlp downloads | — | | FORCE_PROXY_SERVICES | no | additional comma-separated services that must proxy; YouTube and Instagram are always forced | youtube,instagram | | VIDEO_DOWNLOAD_TIMEOUT_MS | no | yt-dlp audio download timeout (ms) | 120000 | | VIDEO_AUDIO_MAX_BYTES | no | max audio size for Cohere (bytes) | 26214400 | | VIDEO_AUDIO_QUALITY | no | audio quality or bitrate | low | | OPENAI_TIMEOUT_MS | no | OpenAI request timeout (ms) | 300000 | | PROVIDER_STEP_DELAY_MS | no | delay before final provider synthesis (ms) | 10000 | | MAX_OUTPUT_TOKENS | no | max provider output tokens for final response | 32768 | | SEARCH_PLAN_MAX_OUTPUT_TOKENS | no | max provider output tokens for search plan generation | 2048 | | VIDEO_TRANSCRIPT_MAX_SEARCHES | no | max Exa searches for video transcript mode (1–20) | 10 | | VIDEO_TRANSCRIPT_RESULTS_PER_SEARCH | no | Exa results per video transcript search (1–10) | 3 | | ARTICLE_MAX_SEARCHES | no | max Exa searches for article/webpage (1–10) | 7 | | ARTICLE_RESULTS_PER_SEARCH | no | Exa results per article/webpage search (1–10) | 4 | | EXA_SEARCH_TEXT_MAX_CHARACTERS | no | max chars per Exa result or source webpage | 35000 | | EXA_SEARCH_TIMEOUT_MS | no | Exa search timeout (ms) | 60000 | | EXA_RETRY_COUNT | no | Exa retry count | 2 | | EXA_RETRY_DELAY_MS | no | Exa retry delay (ms) | 5000 | | DOWNLOADER_RETRY_COUNT | no | downloader retry count | 2 | | DOWNLOADER_RETRY_DELAY_MS | no | downloader retry delay (ms) | 5000 | | COHERE_TRANSCRIBE_MODEL | no | Cohere transcription model | cohere-transcribe-03-2026 | | COHERE_TRANSCRIBE_LANGUAGE | no | ISO-639-1 language code | en | | COHERE_TRANSCRIBE_TIMEOUT_MS | no | Cohere transcription timeout (ms) | 300000 | | COHERE_RETRY_COUNT | no | Cohere retry count | 2 | | COHERE_RETRY_DELAY_MS | no | Cohere retry delay (ms) | 5000 | | DIRECT_MODE_TIMEOUT_MS | no | timeout for direct-mode requests (ms) | 600000 | | LOG_LEVEL | no | pino log level | info in production, debug in development | | NODE_ENV | no | set to `production` for JSON logs | — | | PRETTY_LOGS | no | `true` forces pretty logs, `false` forces JSON | auto | * `COHERE_API_KEY` is required for video URL fact-checking. `PROXY_URL` is required for YouTube, Instagram, and any service listed in `FORCE_PROXY_SERVICES`. Article/webpage fact-checking requires only OpenAI and Exa. Logging The app uses [Pino](https://github.com/pinojs/pino). In development logs are colorized, timestamped, and indented by pino-pretty. In production they are emitted as compact newline-delimited JSON. Every request-scoped log line carries a `requestId`. Large text fields are logged as previews only so logs do not contain full transcripts or responses. Rate limiting Global limits — not per-IP or per-user. | Route | Window variable | Max variable | Default window | Default max | | ----------- | ---------------------------- | -------------------------- | -------------- | ----------- | | /api/check | RATE_LIMIT_WINDOW_MS | RATE_LIMIT_MAX | 86400000 ms | 20 | | /api/health | HEALTH_RATE_LIMIT_WINDOW_MS | HEALTH_RATE_LIMIT_MAX | 500 ms | 1 | Direct response shape { "id": "abc123def456", "inputMode": "url", "url": "https://www.youtube.com/watch?v=VIDEO_ID", "provider": "openai", "model": "gpt-5.6-sol", "models": { "searchPlan": "gpt-5.6-sol", "finalAnswer": "gpt-5.6-sol" }, "effort": "medium", "analysis": "Confidence: 7/10\\n\\nExplanation:\\n...", "reasoning": null, "download": null, "uploadedFile": null, "transcription": { "provider": "cohere", "model": "cohere-transcribe-03-2026", "language": "en", "characterCount": 12345, "audio": { "filename": "audio.mp3", "mimeType": "audio/mpeg", "sizeBytes": 9876543, "iosCompatible": true, "proxy": true, "proxyForced": true, "service": "youtube" } }, "webpage": null, "research": { "provider": "exa", "searchType": "auto", "queries": [{ "query": "specific search query", "rationale": "Why this query is needed" }], "results": [{ "query": "specific search query", "title": "Example Source", "url": "https://example.com", "publishedDate": "2026-01-01", "author": "Example Author" }] }, "usage": { "promptTokenCount": 1000, "candidatesTokenCount": 800, "thoughtsTokenCount": 300, "toolUsePromptTokenCount": null, "totalTokenCount": 1800 }, "warnings": [] } Queued response shape (initial) { "id": "12345678", "ready": false } Queued response shape (complete) { "ready": true, "id": "12345678", "inputMode": "url", "url": "https://...", "provider": "openai", "model": "...", "models": { "searchPlan": "...", "finalAnswer": "..." }, "effort": "...", "analysis": "...", "reasoning": null, "download": null, "uploadedFile": null, "transcription": null, "webpage": null, "research": {}, "usage": {}, "warnings": [] } Setup bun install cp .env.example .env Fill in OPENAI_API_KEY and EXA_API_KEY. Set COHERE_API_KEY for video URL audio transcription. Set PROXY_URL to support YouTube, Instagram, and other proxied downloads. bun run dev — listens on http://localhost:7110