Skip to content
Back to work

Case study

VANTAGE

A technology brief that assembles itself

  • Next.js
  • TypeScript
  • AI synthesis
  • Vercel

01

The problem

Tech news is unreadable. The volume per day is in the hundreds of thousands of articles globally, and most of it is noise, patch notes, gadget sales, recycled press releases, identical takes on the same Reuters wire. I wanted a feed that did the synthesis for me, scored each story by signal strength, and let me skim only what mattered across six regions. I also wanted it to read like an actual editorial voice, Ben Thompson, Matt Levine, The Economist, not like a Twitter bot summarising headlines.

02

How it fits together

One scheduled run, six regions. Each regional collector fires the next on its way out, which is how six regions fit inside a plan that allows one job a day.Drag sideways if it runs past the edge

03

What I read before writing code

  • Read 100+ Stratechery articles to internalise Ben Thompson's structure: every piece is a verdict (the headline IS the thesis), follow the money first, name names, end with a falsifiable prediction. Translated this into an explicit system prompt with banned phrases ('In a move that…', 'It's worth noting…') and required structures.
  • Studied NewsAPI's free tier limits in detail before architecting anything. 100 requests/day, language=en filter, 10 articles per query. That number determined the entire pipeline: I couldn't fan-out to all six regions in parallel, I had to chain them through a single daily run.
  • Read Vercel's Hobby plan limits carefully. Crons run once per day max. Edge function timeout is 60s. These two constraints shaped everything: chain-of-regions through a single cron, every region fits inside 60s, drop the slowest sub-pipelines.
  • Studied the provider's pricing tiers across model families. Sonnet is 3x the cost of Haiku. For a synthesise-and-score pipeline running unattended, Haiku's quality at one-third the cost was the right trade, I lost subtle nuance, kept the editorial voice via the strict prompt.
  • Studied RFC 5005 / RSS specs while building the regional sources. Most regional tech publications still publish RSS even if their websites are bad. Africa especially, TechCabal, Disrupt Africa, Iwacu, etc. RSS was the cheapest way in.

04

What I couldn't do

  • Free Vercel Hobby plan, one cron per day max, edge functions timeout in 60 seconds.
  • Free NewsAPI tier, 100 requests/day total across the whole pipeline.
  • Free Supabase tier, 500 MB DB, no concurrent connection pooling.
  • LLM budget: tight. Every article generated had to be cheap or the system breaks at scale.
  • Editorial voice has to be consistent, no boring summaries, no generic AI prose.

05

The decisions that shaped it

  1. Decision 01

    Chain-of-regions cron, not parallel fetch.

    The single daily cron at 8 AM UTC hits `/api/generate-articles?region=global`. That route, once done, fires off `/api/generate-articles?region=africa` using a chain-secret header. Africa fires asia. Asia fires europe. Six regions, one cron entry, no fan-out, stays inside Vercel free-tier limits while still hitting every region daily. The chain is held together by a fire-and-forget fetch and a shared `x-chain-secret` env var. If one link breaks the chain dies silently, which is acceptable since the next day's run starts the chain over.

  2. Decision 02

    Slug-based de-duplication BEFORE the AI call.

    NewsAPI returns the same headline from multiple sources (Reuters, AP, then 30 outlets quoting them). I slugify the title and check Supabase first, if the slug exists, skip the model entirely. Saves an the LLM provider call per duplicate, which is ~70% of what's returned. The cheapest call is the one you don't make.

  3. Decision 03

    Edge runtime + direct fetch to the LLM provider, no SDK.

    The an LLM Node SDK pulls in dependencies that don't run on Vercel Edge. I wrote a 30-line direct fetch to `the provider's messages endpoint` instead. Edge runtime keeps cold starts under 100ms (vs ~800ms for Node runtime), which matters when six chained route calls are racing the 60-second timeout. Also cuts deployment size.

  4. Decision 04

    Strict editorial system prompt with explicit banned phrases.

    The whole point of VANTAGE is that articles read like editorial, not like 'AI synthesised this for you'. The system prompt is ~400 words of explicit constraints: every headline is a verdict, follow the money first, name names, no em-dashes (use commas/colons/periods only), no 'it's worth noting' / 'interestingly' / 'in a move that…', short paragraphs (2-3 sentences), end with a falsifiable prediction. The output reads sharp because the prompt is sharp.

  5. Decision 05

    Use Haiku for the pipeline, not Sonnet.

    Sonnet was overkill. Article synthesis is high-volume, low-stakes-per-token (you can re-generate tomorrow if today's is mediocre). Haiku at 1/3 the cost lets the pipeline run sustainably on the budget I had. The strict editorial system prompt does the heavy lifting, Haiku follows it well enough for the format.

  6. Decision 06

    Region-by-region article cap (1 per region per run).

    Originally tried to generate 3 articles per region per cron. Hit Vercel's 60s edge timeout repeatedly. Cut to 1 per region per run. Trade-off: less content per day, but the cron actually completes. Better to ship 6 high-signal articles than to time out at article 9 and lose the region's whole batch.

  7. Decision 07

    Disable article deletion when credits run out, even though it bloats the feed.

    Originally a nightly /api/expire cron deleted articles older than 48 hours to keep the feed fresh. When LLM credits exhausted, regeneration stopped, but expiration kept running. I'd wake up to a half-empty feed with no way to refill it. Disabled the expire cron + neutered the route to return `{disabled: true}` even on manual trigger. Better to show old articles than to show nothing.

06

What broke and how I changed course

  • First version stored articles forever. Quickly the feed had 800 articles and the page took 4 seconds to load. Added the /api/expire route to delete >48h. Then ran out of credits, disabled deletion. The feed now persists historic articles intentionally, context as a feature.
  • Pipeline timeout was a recurring battle. First attempt with full prompt at 4000 tokens timed out. Trimmed prompt, then still timed out. Switched Haiku, then fitted. Then added Reddit + HN sub-pipelines, then timed out again. Dropped them. Final: lean RSS-only ingestion, 2500 tokens, Haiku, fits 60s. Half the iterations were 'add capability, blow timeout, remove capability'.
  • NEXT_PUBLIC_SITE_URL was hardcoded to a stale Vercel auto-URL (`vantage-three-chi.vercel.app`) that broke after a project rename. The chain-of-regions silently died because `fetch().catch(() => {})` swallowed the DNS error. No alarm fired; the feed just stopped updating regional content. Took me weeks to notice because global was still working. Lesson: silent fire-and-forget + free-tier observability = bugs that hide for weeks.
  • Found a stray `OneDrive/Desktop/FORGE/` folder accidentally committed inside the vantage repo (probably a Windows OneDrive sync mishap). Vercel build failed with a TypeScript error pointing into FORGE's `prisma.config.ts`. Removed the folder, added it to .gitignore. Cross-project filesystem leakage is a real Windows-specific hazard.

07

What I didn't know, and how I learned

  • I didn't fully understand SSE streaming when I started. Vercel Edge has different streaming semantics than Node. Spent a day debugging why chunks were buffering instead of arriving incrementally. Fix was using `controller.enqueue(encoder.encode(chunk))` instead of trying to write to the response body directly.
  • I underestimated how aggressive Vercel's edge timeout is. 60s sounds like a lot until your pipeline does fetch, then JSON parse, then embedding lookup, then the model call, then DB insert per article. Learning to budget across the chain (under 10s per article, leave headroom for cold start) was a forcing function.
  • I learned the hard way that NewsAPI sorts by `relevancy` differently across regions. 'AI' in `global` returns major AI labs news. 'AI' in `africa` returns 80% Nigerian fintech. Ended up using region-specific keyword sets and RSS feeds for non-global to escape NewsAPI's relevancy bias.

08

What shipped

  • 6 regions × 6 categories = 36 distinct content streams
  • Single daily cron, fully autonomous, runs on Vercel free tier
  • Articles scored 1-100 by signal strength
  • Each article structured: headline (verdict), what happened, why it matters, who wins/loses, what to watch (with falsifiable prediction)
  • Slug-based de-duplication before any AI call
  • Edge runtime + direct the LLM provider fetch (no SDK overhead)
  • Graceful degradation: when credits exhaust, articles persist instead of dying silently

09

What's next

I'd add an embedding-based 'find me articles like this one' next step. Right now the user picks a region/category to filter, but semantic similarity is more useful than topical buckets, readers want 'more on this thread', not 'more in this folder'. Same trick I used in AYAT could ride along here.

What I learned

When you're optimising AI cost, the cheapest call is the one you don't make. De-dup before you spend. When you're optimising on a free tier, treat every constraint (60s timeout, 100 req/day, 500MB DB) as the input to your architecture, not an obstacle. Constraints force creative wiring.