[object Object]

← back to Rentv

PR intel: add Tavily as a 3rd free-tier search backend + PR_SEARCH_BACKEND override

050f7edc15f1b2c6f6431379aa0dd85cc32749d0 · 2026-08-05 16:57:41 -0700 · Steve

Brave's 2k/mo free tier is ~spent, so add Tavily (~1k/mo free, recurring) as a
fallback web-search backend for LinkedIn location. PR_SEARCH_BACKEND=tavily
forces it when Brave is capped. Adapter now: exa -> brave -> tavily. Enrich
tool loads the Tavily key from .env. Needs a free Tavily key (below).

Files touched

Diff

commit 050f7edc15f1b2c6f6431379aa0dd85cc32749d0
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 16:57:41 2026 -0700

    PR intel: add Tavily as a 3rd free-tier search backend + PR_SEARCH_BACKEND override
    
    Brave's 2k/mo free tier is ~spent, so add Tavily (~1k/mo free, recurring) as a
    fallback web-search backend for LinkedIn location. PR_SEARCH_BACKEND=tavily
    forces it when Brave is capped. Adapter now: exa -> brave -> tavily. Enrich
    tool loads the Tavily key from .env. Needs a free Tavily key (below).
---
 src/pr/adapters/search.js       | 20 +++++++++++++++++++-
 src/pr/tools/linkedin-enrich.js |  2 +-
 2 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/src/pr/adapters/search.js b/src/pr/adapters/search.js
index 63359f35..23278fc8 100644
--- a/src/pr/adapters/search.js
+++ b/src/pr/adapters/search.js
@@ -11,9 +11,15 @@ const { throttled, report, register } = require('./index');
 // so the secrets-manager routing works without a PR_-prefixed alias.
 const EXA_KEY = process.env.PR_EXA_API_KEY || process.env.EXA_API_KEY;
 const BRAVE_KEY = process.env.PR_BRAVE_API_KEY || process.env.BRAVE_SEARCH_API_KEY;
+const TAVILY_KEY = process.env.PR_TAVILY_API_KEY || process.env.TAVILY_API_KEY; // free tier ~1k/mo
+const KEYS = { exa: EXA_KEY, brave: BRAVE_KEY, tavily: TAVILY_KEY };
 function backend() {
+  // PR_SEARCH_BACKEND forces one (e.g. =tavily when Brave's monthly free tier is spent).
+  const forced = process.env.PR_SEARCH_BACKEND;
+  if (forced && KEYS[forced]) return forced;
   if (EXA_KEY) return 'exa';
   if (BRAVE_KEY) return 'brave';
+  if (TAVILY_KEY) return 'tavily';
   return null;
 }
 
@@ -40,6 +46,18 @@ async function braveSearch(query, { numResults = 10, cursor } = {}) {
   return (((j.web || {}).results) || []).map((x) => ({ title: x.title, url: x.url, snippet: x.description || '', published_at: x.age || null }));
 }
 
+async function tavilySearch(query, { numResults = 10 } = {}) {
+  const r = await fetch('https://api.tavily.com/search', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ api_key: TAVILY_KEY, query, max_results: numResults, search_depth: 'basic', include_answer: false }),
+    signal: AbortSignal.timeout(20000),
+  });
+  if (!r.ok) throw new Error('tavily ' + r.status + ': ' + (await r.text()).slice(0, 200));
+  const j = await r.json();
+  return (j.results || []).map((x) => ({ title: x.title, url: x.url, snippet: (x.content || '').slice(0, 300), published_at: x.published_date || null }));
+}
+
 const adapter = register({
   name: 'search',
   kind: 'discovery',
@@ -61,7 +79,7 @@ const adapter = register({
     }
     const errors = []; let items = [];
     try {
-      items = await throttled('search', () => (be === 'exa' ? exaSearch(query, opts) : braveSearch(query, opts)));
+      items = await throttled('search', () => (be === 'exa' ? exaSearch(query, opts) : be === 'tavily' ? tavilySearch(query, opts) : braveSearch(query, opts)));
     } catch (e) { errors.push({ error: e.message }); }
     return report({
       provider: 'search:' + be, query, errors, primary_source: false,
diff --git a/src/pr/tools/linkedin-enrich.js b/src/pr/tools/linkedin-enrich.js
index ed40b1c1..61b9540b 100644
--- a/src/pr/tools/linkedin-enrich.js
+++ b/src/pr/tools/linkedin-enrich.js
@@ -18,7 +18,7 @@ const path = require('path');
 try {
   const envTxt = fs.readFileSync(path.join(__dirname, '../../../.env'), 'utf8');
   for (const line of envTxt.split('\n')) {
-    const m = line.match(/^\s*(BRAVE_SEARCH_API_KEY|PR_BRAVE_API_KEY|PR_EXA_API_KEY|EXA_API_KEY)\s*=\s*(.+)\s*$/);
+    const m = line.match(/^\s*(BRAVE_SEARCH_API_KEY|PR_BRAVE_API_KEY|PR_EXA_API_KEY|EXA_API_KEY|TAVILY_API_KEY|PR_TAVILY_API_KEY)\s*=\s*(.+)\s*$/);
     if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
   }
 } catch { /* env optional */ }

← 214a147e directory density: Cody gate fixes — hide slider on mobile (  ·  back to Rentv  ·  blog: add sort (Newest/Oldest/Title/Category) + density slid fc0ee30d →