Updates Feed
A chronological stream of items delivered by Exa Monitors. Each item is a deduped result from a recurring search, tagged by monitor, with source and timestamp. The right surface for buying signals, competitor moves, and account watchlists.
Demo
Three demo monitors are running: AI funding, Cursor activity, and sales tech launches. Hit Refresh to fan out one search per monitor and merge results into a single sorted feed.
How it works
Each row in the feed is a deduplicated event surfaced by an Exa Monitor. Each monitor runs the same search on a schedule and POSTs its results to your webhook.
The outputSchema asks Exa to synthesize up to 3 distinct events per run with headline, summary, sourceUrl, and publishedDate, not a dump of every matching article.
Refresh calls exa.monitors.trigger on each monitor to fire a fresh run.
Events from every monitor merge into one sorted stream. Each carries a monitorId so the UI can color-code its source.
Source code
"use client";
import { useEffect, useState } from "react";
import {
DEMO_MONITORS,
type FeedItem,
type Monitor,
} from "@/lib/feed-monitors";
export function FeedExample() {
const [items, setItems] = useState<FeedItem[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastRefreshed, setLastRefreshed] = useState<number | null>(null);
const [readIds, setReadIds] = useState<Set<string>>(new Set());
// Initial load: read the most recent completed run for each
// monitor. Fast (no trigger, no polling).
useEffect(() => {
void load(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function load(refresh: boolean) {
if (loading || refreshing) return;
if (refresh) setRefreshing(true);
else setLoading(true);
setError(null);
try {
const res = await fetch("/api/feed", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ monitors: DEMO_MONITORS, refresh }),
});
const json = await res.json();
if (!res.ok || json.error) {
setError(json.error || `Request failed (${res.status})`);
return;
}
setItems(json.items ?? []);
if (refresh) setLastRefreshed(Date.now());
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
setRefreshing(false);
}
}
function refresh() {
void load(true);
}
function markRead(id: string) {
setReadIds((prev) => {
if (prev.has(id)) return prev;
const next = new Set(prev);
next.add(id);
return next;
});
}
const hasItems = items.length > 0;
return (
<div className="border border-[#e5e5e5] bg-white overflow-hidden">
<div className="flex items-center justify-between border-b border-[#e5e5e5] bg-[#fafafa] px-3 py-2 gap-2">
<div className="flex flex-wrap items-center gap-1.5 min-w-0">
{DEMO_MONITORS.map((m) => (
<MonitorPill key={m.id} monitor={m} />
))}
</div>
<div className="flex items-center gap-2 shrink-0">
{lastRefreshed && (
<span className="text-[11px] text-[#9ca3af]">
Refreshed {formatRelative(lastRefreshed)}
</span>
)}
<button
type="button"
onClick={refresh}
disabled={loading || refreshing}
className="bg-[var(--color-brand)] text-white text-[12px] font-medium px-3 py-1 disabled:opacity-50"
>
{refreshing ? "Refreshing..." : "Refresh"}
</button>
</div>
</div>
<div className="max-h-[520px] overflow-y-auto">
{error && (
<div className="px-4 py-3 text-[13px] text-[#b91c1c]">{error}</div>
)}
{!error && !hasItems && !loading && !refreshing && (
<div className="px-4 py-12 text-center text-[13px] text-[#9ca3af]">
Hit Refresh to fan out the demo monitors.
</div>
)}
{!error && !hasItems && (loading || refreshing) && <FeedSkeleton />}
{items.map((item) => (
<FeedRow
key={item.id}
item={item}
read={readIds.has(item.id)}
onOpen={() => markRead(item.id)}
/>
))}
</div>
</div>
);
}
function MonitorPill({ monitor }: { monitor: Monitor }) {
return (
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${monitor.color}`}
>
{monitor.name}
</span>
);
}
function FeedRow({
item,
read,
onOpen,
}: {
item: FeedItem;
read: boolean;
onOpen: () => void;
}) {
const monitor = DEMO_MONITORS.find((m) => m.id === item.monitorId);
return (
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
onClick={onOpen}
className={`block px-4 py-3 border-b border-[#e5e5e5] last:border-b-0 transition-colors hover:bg-[#fafafa] ${read ? "opacity-60" : ""}`}
>
<div className="flex items-center gap-2 mb-1.5">
{monitor && <MonitorPill monitor={monitor} />}
</div>
<div className="text-[14px] font-medium text-[#111827] leading-[1.4] mb-1">
{item.title}
</div>
{item.blurb && (
<div className="text-[12px] text-[#6b7280] leading-[1.55] mb-1.5 line-clamp-2">
{item.blurb}
</div>
)}
<div className="flex items-center gap-1.5 text-[11px] text-[#9ca3af]">
<img
src={`https://www.google.com/s2/favicons?domain=${item.source}&sz=32`}
alt=""
className="h-3 w-3"
loading="lazy"
/>
<span>{item.source}</span>
<span>·</span>
<span>{formatItemDate(item.publishedDate)}</span>
</div>
</a>
);
}
function FeedSkeleton() {
return (
<div className="px-4 py-3 space-y-4">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="space-y-1.5">
<div className="h-3 w-20 rounded bg-[#e5e7eb] animate-pulse" />
<div className="h-4 w-2/3 rounded bg-[#e5e7eb] animate-pulse" />
<div className="h-3 w-full rounded bg-[#e5e7eb] animate-pulse" />
</div>
))}
</div>
);
}
function formatItemDate(publishedDate: string | undefined): string {
if (!publishedDate) return "Today";
const ts = Date.parse(publishedDate);
if (isNaN(ts)) return "Today";
const diffDays = Math.floor((Date.now() - ts) / (1000 * 60 * 60 * 24));
if (diffDays <= 0) return "Today";
return formatRelative(ts);
}
function formatRelative(ts: number): string {
const diff = Date.now() - ts;
if (isNaN(diff)) return "";
const sec = Math.floor(diff / 1000);
if (sec < 60) return `${Math.max(1, sec)}s ago`;
const min = Math.floor(sec / 60);
if (min < 60) return `${min}m ago`;
const hr = Math.floor(min / 60);
if (hr < 24) return `${hr}h ago`;
const day = Math.floor(hr / 24);
if (day < 30) return `${day}d ago`;
const mo = Math.floor(day / 30);
if (mo < 12) return `${mo}mo ago`;
return `${Math.floor(mo / 12)}y ago`;
}