The daily top 5 is served as JSON at /api/ai-news.json —
open (CORS *), GET, cached 30 min, refreshed daily ~06:30 UTC. Reuse with
attribution to solvX and the original source.
Response shape
{
"service": "solvX AI Briefing",
"date": "2026-06-29", // YYYY-MM-DD (UTC)
"generatedAt": "2026-06-29T06:31:00Z",
"count": 5,
"stories": [
{
"rank": 1, // number, 1..5 ascending (rank 1 = top story)
"title": "string", // full original headline
"url": "string", // canonical http(s) link
"source": "string", // e.g. "OpenAI News"
"category": "string", // per-story chip, e.g. "SECURITY", "HARDWARE", "BUSINESS"
"tags": ["string"], // array of strings
"summary": "string", // plain text, 1-3 sentences
"publishedAt": "string", // original publish timestamp
// Video card fields. ON-SCREEN text (read) and VOICEOVER (heard) are
// DIFFERENT content by design — the voiceover never recites the on-screen lines:
"headline": "string", // on-screen card title, <= 40 chars
"beats": ["string","string"], // on-screen: EXACTLY 2 facts, each <= 42 chars
"takeaway": "string", // on-screen "why it matters", <= 70 chars
"voiceover": "string", // SPOKEN narration, 2-3 sentences (~35-55 words), distinct from on-screen
// v4 — depth + background sources:
"explainer": "string", // on-screen CONTEXT the viewer reads, 2-3 sentences (~35-75 words)
"entities": ["string"], // canonical company/product names for a REAL logo lookup; [] if none
"image_keywords": ["string"],// 2-4 concrete visual nouns for a fallback image (no brands/text); [] if none
// v5 — brand-arrival stinger (used when the story has no real logo):
"bg_word": "string", // short subject to show, 1-3 words (~18 chars), Title Case, non-empty
"bg_tone": "string", // one of: "launch" | "hot" | "money" | "shock" | "bad" | "neutral"
// v6 — what/why split for the video:
"so_what": "string", // ONE-sentence implication (<=25 words), the "so what" — does NOT restate the event
"detail": "string" // one concrete anchor fact (<=24 chars): a number, version, price or proper name
}
// ... 5 stories total
]
}
Fetch example
const res = await fetch("https://solvx.uk/api/ai-news.json");
const { date, stories } = await res.json();
// stories[] is rank-ordered 1 to 5; map each onto a countdown card with no transform
Field guarantees
On every story: title, url, rank, headline,
takeaway, voiceover and explainer are non-empty; beats
is an array of EXACTLY 2 non-empty items; entities and image_keywords are always
arrays (possibly empty []); all five stories share an identical key set. headline,
beats and takeaway are the SHORT on-screen text; explainer is the
longer on-screen CONTEXT the viewer reads to understand the story (distinct from the beats); and
voiceover is flowing SPOKEN narration written for a different channel — it adds context and
does NOT duplicate the on-screen lines. entities holds canonical company/product names for a
logo lookup; image_keywords holds concrete visual nouns (never brand names or text) for a
fallback background image. bg_word (non-empty, 1-3 words) and bg_tone drive the
per-story "brand arrival" stinger shown when there is no real logo — bg_tone is always exactly
one of launch, hot, money, shock, bad or
neutral. so_what is a non-empty ONE-sentence implication (≤ 25 words) — the
"why it matters", written to NOT restate the headline/beats/takeaway/explainer/summary (an overlap check
swaps in a consequence line if it would); detail is one concrete anchor fact (≤ 24 chars — a
number, version, price or proper name) taken from the story. The card fields and
summary are plain text — HTML entities decoded, no markdown, no newlines — and length-capped
(truncated at a word boundary, never overflowed) so they drop straight into a fixed-size card.