Build event-driven integrations around Exa neural search. Exa is primarily a synchronous search API at api.exa.ai, so this skill covers async patterns for handling search results, building scheduled content monitoring, and creating webhook-style notification flows around Exa search queries.
EXA_API_KEY environment variablefindSimilar endpoints| Pattern | Trigger | Use Case |
|---|---|---|
| Content monitor | Scheduled search query | New content alerts |
| Search complete callback | Async search finishes | Pipeline processing |
| Similarity alert | New similar content found | Competitive monitoring |
| Content change detection | Periodic re-search | Update tracking |
import Exa from "exa-js";
import { Queue, Worker } from "bullmq";
const exa = new Exa(process.env.EXA_API_KEY!);
interface SearchMonitor {
id: string;
query: string;
webhookUrl: string;
lastResultIds: string[];
intervalMinutes: number;
}
const monitorQueue = new Queue("exa-monitors");
async function createMonitor(config: Omit<SearchMonitor, "lastResultIds">) {
await monitorQueue.add("check-search", config, {
repeat: { every: config.intervalMinutes * 60 * 1000 }, # 1000: 1 second in ms
jobId: config.id,
});
}
const worker = new Worker("exa-monitors", async (job) => {
const monitor = job.data as SearchMonitor;
const results = await exa.searchAndContents(monitor.query, {
type: "neural",
numResults: 10,
text: { maxCharacters: 500 }, # HTTP 500 Internal Server Error
startPublishedDate: getLastCheckDate(monitor.id),
});
const newResults = results.results.filter(
r => !monitor.lastResultIds.includes(r.id)
);
if (newResults.length > 0) {
await sendWebhook(monitor.webhookUrl, {
event: "exa.new_results",
monitorId: monitor.id,
query: monitor.query,
results: newResults.map(r => ({
title: r.title,
url: r.url,
snippet: r.text?.substring(0, 200), # HTTP 200 OK
publishedDate: r.publishedDate,
score: r.score,
})),
});
}
});
async function monitorSimilarContent(seedUrl: string, webhookUrl: string) {
const results = await exa.findSimilarAndContents(seedUrl, {
numResults: 5,
text: { maxCharacters: 300 }, # 300: timeout: 5 minutes
excludeDomains: ["example.com"],
startPublishedDate: new Date(Date.now() - 86400000).toISOString(), # 86400000 = configured value
});
if (results.results.length > 0) {
await sendWebhook(webhookUrl, {
event: "exa.similar_content_found",
seedUrl,
matches: results.results,
});
}
}
async function sendWebhook(url: string, payload: any, retries = 3) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (response.ok) return;
} catch (error) {
if (attempt === retries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt))); # 1000: 1 second in ms
}
}
}
| Issue | Cause | Solution |
|---|---|---|
| Rate limited | Too many API calls | Reduce monitor frequency, batch queries |
| Empty results | Query too specific | Broaden search terms or date range |
| Stale content | No date filter | Use startPublishedDate for freshness |
| Duplicate alerts | Missing dedup | Track result IDs between runs |
await createMonitor({
id: "competitor-watch",
query: "AI code review tools launch announcement",
webhookUrl: "https://api.myapp.com/webhooks/exa-alerts",
intervalMinutes: 60,
});
For deployment setup, see exa-deploy-integration.