Ga naar hoofdinhoud

Perplexity Search API-tutorial: bouw een realtime Claim Checker in je browser

Leer hoe je de Perplexity Search API gebruikt om een Chrome-extensie te bouwen die feitelijke claims in realtime verifieert. Inclusief Node.js-backend en handleiding voor API-integratie.
Bijgewerkt 2 okt 2026  · 10 min lezen

Verken met AI

ChatGPTClaudePerplexity

Perplexity heeft onlangs de Perplexity Search API uitgebracht, ontworpen voor fijnmazige webopvraging die gerangschikte, contextrijke snippets retourneert in plaats van volledige pagina’s. De API is gebouwd voor realtime use-cases, zoals factchecking, onderzoek en RAG-pijplijnen, waar snelheid en precisie tellen.

In deze tutorial bouwen we Claim Checker, een browserextensie die feitelijke uitspraken op elke webpagina in realtime verifieert (op basis van opgehaalde webbewijzen). Je ziet hoe de voortdurend ververste index en de low-latency retrievalstack van de Perplexity Search API, gecombineerd met semantische signalen en meertraps her-ranking, het eenvoudig maken om snel het meest relevante bewijs te vinden. Onderweg leer je hoe je:

  • De Perplexity Search API gebruikt om hoogwaardige, gerangschikte bewijzen van het web op te halen en zoekresultaten te verwerken met aangepaste sorteeralgoritmen.
  • Een Node.js-backend ontwikkelt om je API-sleutel veilig te beheren, CORS-verzoeken af te handelen en slimme resultaatverwerking te implementeren met domeinextractie en datumgebaseerde ranking.
  • Een Chrome/Edge-extensie maakt met content scripts die tekstselectie, zijpaneelinterfaces en naadloze communicatie tussen de browser en je backendserver mogelijk maken.
  • Verificatieresultaten toont, inclusief bron-URL’s, titels, snippets, domeinen en publicatiedata in een interactief zijpaneel met realtime claimverwerking.

Aan het einde heb je een full-stack factcheckingtool die naadloos binnen je browser werkt. Gebruikers kunnen elke tekst op een webpagina selecteren en direct verificatieresultaten met ondersteunende bronnen krijgen, terwijl API-gegevens veilig aan de serverzijde blijven.

Je kunt ook ons artikel over Perplexity’s Comet Browser in een aparte gids bekijken. 

Wat is de Perplexity Search API?

De Perplexity Search API is een ontwikkelaarsvriendelijke manier om snel en fijnmazig webzoekresultaten te krijgen. 

In tegenstelling tot traditionele zoek-API’s die alleen volledige pagina’s teruggeven, levert Perplexity gerangschikte snippets en subdocumentfragmenten met de exacte zinnen en alinea’s die overeenkomen met je query. Enkele belangrijke kenmerken van deze search API zijn:

  • Kostenefficiënt: De Search API kost $5 per 1.000 requests zonder tokengebaseerde facturatie, ideaal voor apps die vaak of volgens schema checks uitvoeren.
  • Hoge kwaliteit van retrieval: Combineert lexicale en semantische ranking met meertraps her-ranking, zodat je zeer relevante, actuele resultaten krijgt uit een enorme, continu ververste index (~200M queries/dag).
  • Lage latentie: De Perplexity Search API verwerkt queries snel, waardoor je UI responsief blijft, zelfs bij het verifiëren van meerdere claims tegelijk.

Voor onze extensie betekent dit dat we elke zin als query kunnen doorgeven en betrouwbare ondersteunende of tegensprekende bewijzen terugkrijgen, compleet met titels, URL’s en contextsnippets — perfect voor realtime factchecking.

Perplexity Search API-project: Browser Claim Checker-extensie

In deze sectie bouwen we een browserextensie voor claimcontrole die feitelijke beweringen direct verifieert met de Perplexity Search API. Dit project combineert:

  • Een lichte Node.js-backend die geselecteerde tekst naar de Perplexity Search API stuurt en de resultaten verwerkt
  • Een Chrome-extensie met een zijpaneelinterface om verificatiedetails te tonen
  • Een content script dat door de gebruiker geselecteerde tekst van elke webpagina opvangt

Aan het einde van het project ziet je repository-structuur er zo uit:

ClaimChecker/
├── server/
│   ├── server.js
│   ├── package.json
│   └── .env
├── extension/
│   ├── manifest.json
│   ├── background.js
│   ├── content.js
│   ├── sidepanel.html
│   ├── sidepanel.js
│   ├── popup.html
│   └── popup.js
└── README.md

Laten we elk onderdeel van dit project doornemen en zien hoe ze samenwerken om betrouwbare claimverificatie in de browser te leveren.

Stap 1: Afhankelijkheden en bibliotheken 

Zorg voordat je begint dat je het volgende geregeld hebt:

  • Een Perplexity-account. Nadat je je hebt aangemeld of ingelogd, ga je in de linkerzijbalk naar de API-sectie om je API-groep en gerelateerde instellingen te openen.

  • Ga naar het tabblad API Billing in je API-portaal en voeg je betaalmethode toe. Voor deze demo is ongeveer $10 aan tegoed voldoende om de requests te dekken.
  • Node.js versie 18 of hoger.
  • Chromium-gebaseerde browser (Google Chrome of Microsoft Edge).

Installeer vervolgens de vereiste afhankelijkheden voor de backend:

cd server
npm install express cors dotenv

Maak een .env-bestand in de servermap en voeg je Perplexity API-sleutel toe:

PERPLEXITY_API_KEY=your_api_key_here

Nu zijn alle afhankelijkheden geïnstalleerd. Vervolgens bouwen we onze backendserver en extensie.

Stap 2: Bouw de backend

Het bestand server.js zet een lichte Node.js-backend op met Express. Deze backend fungeert als brug tussen de browserextensie en de Perplexity Search API. Hij ontvangt claims die door de gebruiker zijn geselecteerd of ingevoerd, vraagt bij Perplexity naar relevant bewijs en retourneert een opgeschoond, gestructureerd antwoord dat klaar is voor weergave in de UI van de extensie.

const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = 3000;
app.use(cors());
app.use(express.json());
function extractDomain(url) {
  try {
    const urlObj = new URL(url);
    return urlObj.hostname.replace('www.', '');
  } catch {
    return url;
  }
}
app.post('/check-claim', async (req, res) => {
  try {
    const { claim } = req.body;
    if (!claim) {
      return res.status(400).json({ error: 'Claim is required' });
    }
    if (!process.env.PERPLEXITY_API_KEY) {
      return res.status(500).json({ 
        error: 'Perplexity API key not configured',
        sources: []
      });
    }
    const response = await fetch('https://api.perplexity.ai/search', {
      method: 'POST',
      headers: {
        'Authorization': Bearer ${process.env.PERPLEXITY_API_KEY},
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        query: claim,
        max_results: 20,
        max_tokens_per_page: 1024
      })
    });
    if (!response.ok) {
      throw new Error(Perplexity API error: ${response.status});
    }
    const data = await response.json();
    const searchResults = data.results || [];   
    if (searchResults.length === 0) {
      return res.json({
        claim,
        sources: [],
        totalSources: 0,
        explanation: 'No relevant sources found to verify this claim.',
        summary: 'No sources found for fact-checking.'
      });
    }
    const sortedResults = searchResults.sort((a, b) => {
      const dateA = new Date(a.last_updated || a.date || 0);
      const dateB = new Date(b.last_updated || b.date || 0);
      return dateB - dateA;
    });
    const sources = sortedResults.slice(0, 3).map((result) => ({
      url: result.url,
      title: result.title,
      snippet: result.snippet || '',
      domain: extractDomain(result.url),
      date: result.date || null,
      lastUpdated: result.last_updated || null
    }));
    const result = {
      claim,
      sources,
      totalSources: searchResults.length,
      explanation: ${searchResults.length} sources found, showing top 3.,
      summary: Found ${searchResults.length} relevant sources for fact-checking.
    };
    res.json(result);
  } catch (error) {
    res.status(500).json({ 
      error: error.message,
      sources: []
    });
  }
});
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});
app.listen(PORT, () => {
  console.log(Server running on http://localhost:${PORT});
  console.log('Make sure PERPLEXITY_API_KEY is set in .env file');
});

Bovenstaande code is de beveiligings- en logische laag voor de Claim Checker-extensie en doet het volgende:

  • Veilige en logische backend: Accepteert een claim via de route POST /check-claim, valideert de invoer en roept de Perplexity Search API aan met een serverside API-sleutel.
  • Verwerken en structureren: De server ontvangt een lijst met relevante webbronnen en snippets, sorteert ze op actualiteit met last_updated en datumvelden, selecteert de beste drie en retourneert een nette JSON-respons voor de extensie.
  • Sleutelbeveiliging: Door deze backend te draaien blijft je API-sleutel verborgen voor de browser en kun je extra logica toevoegen, zoals ranking, filtering of caching, voordat gegevens de gebruiker bereiken.
  • Uitbreidbaar ontwerp: Deze aanpak maakt de extensie veiliger en gemakkelijk uit te breiden met toekomstige verbeteringen zoals geavanceerde scoring, samenvoegen van bronnen of analytics.

De server is klaar. Laten we een Chrome-extensie bouwen.

Stap 3: De Chrome-extensie bouwen

In deze stap bouwen we de Chrome-extensie die de gebruikerservaring van Claim Checker aanstuurt. De extensie is verantwoordelijk voor het vastleggen van claims van elke webpagina, ze naar onze backend sturen en de verificatieresultaten tonen in een intuïtieve interface.

Voordat we in elk bestand duiken, volgt hier wat deze kernonderdelen doen:

  • background.js: Dit script draait als service worker en beheert achtergrondtaken zoals luisteren naar gebruikersacties, het openen van het zijpaneel en het veilig naar de backend sturen van claims voor verificatie.
  • content.js:  Dit content script draait binnen de actieve webpagina. Het extraheert de zichtbare artikeltekst, identificeert mogelijke feitelijke claims en markeert deze dynamisch zodat de gebruiker ziet wat wordt gecontroleerd.
  • sidepanel.js: Stuurt de interactieve UI van het zijpaneel aan die de geëxtraheerde claims en hun verificatiestatus toont. Werkt resultaten in realtime bij en koppelt ze aan de highlights op de pagina.
  • popup.js: Stuurt de snelle pop-up aan wanneer de gebruiker op het extensiepictogram klikt. Biedt een lichte manier om claims te checken zonder het zijpaneel te openen.

Je vindt een paar extra helperbestanden voor de Chrome-extensie in de GitHub-repository.

Stap 3.1: Backgroundscript en event handling 

Voortbouwend op onze Chrome-extensiesetup is het bestand background.js de centrale controller die de extensie responsief houdt. Het luistert naar gebruikersacties (zoals het klikken op het extensiepictogram), opent indien nodig het zijpaneel en stuurt claims door naar de backend voor verificatie.

const SERVER_URL = 'http://localhost:3000';
chrome.action.onClicked.addListener(async (tab) => {
  try {
    await chrome.sidePanel.open({ tabId: tab.id });
  } catch (error) {
    console.error('Error opening side panel:', error);
    chrome.action.setPopup({ popup: 'popup.html' });
  }
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'checkClaim') {
    checkClaim(request.claim)
      .then(result => sendResponse({ success: true, result }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    return true; 
  }
});
async function checkClaim(claim) {
  try {
    const response = await fetch(${SERVER_URL}/check-claim, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ claim })
    });
    if (!response.ok) {
      throw new Error(Server error: ${response.status});
    }
    return await response.json();
  } catch (error) {
    console.error('Error checking claim:', error);
    throw error;
  }
}

Bovenstaande code richt zich op de responsiviteit van de extensie door het volgende te integreren:

  • Centraal eventknooppunt: Het backgroundscript registreert twee hoofdlisteners, chrome.action.onClicked en chrome.runtime.onMessage. De eerste reageert wanneer de gebruiker op het extensiepictogram klikt en probeert het zijpaneel te openen. De tweede luistert naar aangepaste berichten zoals checkClaim vanuit het content script of UI-componenten.
  • Veilige API-afhandeling: De functie checkClaim() gebruikt de fetch-API om de geselecteerde claim naar de backend te sturen op http://localhost:3000/check-claim; omdat de API-sleutel aan de serverzijde is opgeslagen, wordt deze nooit in de browser blootgesteld en blijven gevoelige gegevens veilig.
  • Foutbestendigheid: Beide eventlisteners bevatten try/catch-blokken om runtimefouten af te handelen. Als het zijpaneel niet kan worden geopend, valt het script terug op het instellen van de pop-up. Wanneer API-calls mislukken, worden gestructureerde foutmeldingen gelogd en teruggestuurd naar de UI, zodat gebruikers niet met stille fouten blijven zitten.

Met het backgroundscript dat communicatie en veilige API-calls afhandelt, is de volgende stap interactie met de webpagina zelf.

Stap 3.2: Claims extraheren en markeren

Het bestand content.js draait direct binnen de webpagina en is verantwoordelijk voor het extraheren van artikeltekst, het identificeren van mogelijke feitelijke claims en het visueel markeren ervan voor de gebruiker. Het slaat de brug tussen de paginacontent en de backgroundlogica van de extensie door relevante tekst vast te leggen en gebruikersinteractie mogelijk te maken.

function extractArticleText() {
    const selectors = [
      'article',
      '[role="article"]',
      '.article-content',
      '.post-content',
      '.entry-content',
      'main'
    ];  
    for (const selector of selectors) {
      const element = document.querySelector(selector);
      if (element) {
        return element.innerText;
      }
    }  
    return document.body.innerText;
  }  
  function extractClaims(text) {
    const sentenceMatches = text.match(/[^.!?]*[.!?]+/g) || [];
    const sentences = sentenceMatches
      .map(s => s.trim())
      .filter(s => s.length > 20 && s.length < 1000);
    const claimKeywords = [
      /\d+%/, 
      /\d+\s+(million|billion|thousand|trillion)/i, 
      /according to/i,
      /study shows/i,
      /research found/i,
      /scientists/i,
      /experts/i,
      /will|would|could|should/i,
      /increase|decrease|rise|fall/i,
      /more than|less than|at least/i,
      /proven|showed|demonstrated/i,
      /investment|invest|create|generate/i,  
      /jobs|employment|economy/i,  
      /ripple effect|impact|effect/i  
    ];  
    const claims = [];    
    for (const sentence of sentences) {
      const hasClaimKeyword = claimKeywords.some(regex => regex.test(sentence));      
      const isValidLength = sentence.length > 30 && sentence.length < 800;
      const notNavigation = !/(click here|read more|subscribe|sign up|menu|home|cookie|privacy)/i.test(sentence);
      const hasCompleteThought = /[.!?]$/.test(sentence) || sentence.length > 50;      
      if (hasClaimKeyword && isValidLength && notNavigation && hasCompleteThought) {
        let cleanSentence = sentence
          .replace(/\s+/g, ' ')  
          .replace(/^\s*[-•]\s*/, '') 
          .trim();        
        if (cleanSentence.length > 30) {
          claims.push(cleanSentence);
        }
      }
    }  
    return claims.slice(0, 10);
  }  
let highlightedElements = new Map();
let claimIdCounter = 0;
function highlightTextOnPage(claim, claimId) {
  const text = document.body.innerText;
  const claimWords = claim.toLowerCase().split(/\s+/).filter(word => word.length > 3);  
  if (claimWords.length < 3) return null;
  const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 20);
  let bestMatch = null;
  let bestScore = 0;  
  for (const sentence of sentences) {
    const sentenceWords = sentence.toLowerCase().split(/\s+/);
    let score = 0;    
    for (const word of claimWords) {
      if (sentenceWords.some(sw => sw.includes(word) || word.includes(sw))) {
        score++;
      }
    }    
    if (score > bestScore && score >= Math.min(3, claimWords.length * 0.5)) {
      bestScore = score;
      bestMatch = sentence;
    }
  }  
  if (!bestMatch) return null;  
  const walker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );  
  let node;
  while (node = walker.nextNode()) {
    if (node.textContent.includes(bestMatch)) {
      const parent = node.parentElement;
      if (parent && !parent.classList.contains('claim-highlight')) {
        const highlight = document.createElement('span');
        highlight.className = 'claim-highlight';
        highlight.id = claim-${claimId};
        highlight.style.cssText = 
          background: linear-gradient(120deg, #fbbf24 0%, #f59e0b 100%);
          
          border-radius: 3px;
          box-shadow: 0 1px 3px rgba(0,0,0,0.2);
          cursor: pointer;
          transition: all 0.2s ease;
          position: relative;
        ;        
        const textContent = node.textContent;
        const index = textContent.indexOf(bestMatch);
        if (index !== -1) {
          const before = textContent.substring(0, index);
          const after = textContent.substring(index + bestMatch.length);         
          const beforeNode = document.createTextNode(before);
          const afterNode = document.createTextNode(after);          
          highlight.textContent = bestMatch;
          node.parentNode.insertBefore(beforeNode, node);
          node.parentNode.insertBefore(highlight, node);
          node.parentNode.insertBefore(afterNode, node);
          node.parentNode.removeChild(node);
          highlight.addEventListener('mouseenter', () => {
            highlight.style.transform = 'scale(1.05)';
            highlight.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)';
          });    
          highlight.addEventListener('mouseleave', () => {
            highlight.style.transform = 'scale(1)';
            highlight.style.boxShadow = '0 1px 3px rgba(0,0,0,0.2)';
          });   
          highlight.addEventListener('click', () => {
            chrome.runtime.sendMessage({
              action: 'highlightClaimInPanel',
              claimId: claimId
            });        
            highlight.style.background = 'linear-gradient(120deg, #10b981 0%, #059669 100%)';
            setTimeout(() => {
              highlight.style.background = 'linear-gradient(120deg, #fbbf24 0%, #f59e0b 100%)';
            }, 1000);
          });     
          highlightedElements.set(claimId, highlight);
          return claimId;
        }
      }
      break;
    }
  }  
  return null;
}
function removeHighlights() {
  highlightedElements.forEach((element, claimId) => {
    if (element && element.parentNode) {
      const parent = element.parentNode;
      const text = element.textContent;
      const textNode = document.createTextNode(text);
      parent.insertBefore(textNode, element);
      parent.removeChild(element);
    }
  });
  highlightedElements.clear();
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'extractClaims') {
    const text = extractArticleText();
    const claims = extractClaims(text);
    sendResponse({ claims, url: window.location.href });
  } else if (request.action === 'highlightClaim') {
    const { claim, claimId } = request;
    const highlightId = highlightTextOnPage(claim, claimId);
    sendResponse({ success: true, highlightId });
  } else if (request.action === 'removeHighlights') {
    removeHighlights();
    sendResponse({ success: true });
  }
  return true;
});

Laten we de belangrijkste onderdelen van het script content.js bekijken om te begrijpen hoe het betekenisvolle tekst extraheert, mogelijke claims detecteert en de gemarkeerde content verbindt met de UI van de extensie.

  • Artikeltekst extraheren: De functie extractArticleText() scant veelvoorkomende artikelcontainers om de hoofdinhoud op te halen. Zo richt de extensie zich op betekenisvolle tekst in plaats van navigatiemenu’s of advertenties.
  • Claimdetectielogica: De functie extractClaims() splitst tekst in zinnen, past filters toe voor lengte en structuur en gebruikt regex-gebaseerde trefwoorden om uitspraken te detecteren die waarschijnlijk feitelijke claims zijn.
  • Dynamisch markeren: De functie highlightTextOnPage() lokaliseert de gedetecteerde claim binnen de DOM en wikkelt deze in een gestileerde <span>. Er worden hover- en klikinteracties toegevoegd zodat gebruikers claims op de pagina visueel kunnen koppelen aan hun verificatieresultaten in het zijpaneel.
  • Berichtafhandeling: De listener chrome.runtime.onMessage coördineert acties zoals claims extraheren, highlights toevoegen en ze wissen wanneer nodig. Dit houdt de pagina en de extensie gesynchroniseerd en zorgt voor soepele interactie.

Met het contentscript dat tekstopname en claimmarkering op de pagina verzorgt, is de volgende stap om die claims en hun verificatieresultaten aan de gebruiker te presenteren. Hier komt de interface sidepanel.js in beeld.

Stap 3.3: Zijpaneel-UI

Het bestand sidepanel.js drijft het interactieve zijpaneel aan waar gebruikers geëxtraheerde claims en hun verificatieresultaten kunnen bekijken. Het verbindt het contentscript en backendantwoorden met een duidelijke, scrollbare interface binnen de extensie.

let claimCounter = 0;
let currentHighlights = new Map();
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'highlightClaimInPanel') {
    const claimId = request.claimId;
    const claimElement = document.getElementById(sidepanel-${claimId});    
    if (claimElement) {
      claimElement.scrollIntoView({ 
        behavior: 'smooth', 
        block: 'center' 
      });      
      claimElement.classList.add('highlighted');      
      claimElement.style.animation = 'pulse 1s ease-in-out';     
      setTimeout(() => {
        claimElement.classList.remove('highlighted');
        claimElement.style.animation = '';
      }, 3000);
    }
  }
});
document.getElementById('check-btn').addEventListener('click', async () => {
    const button = document.getElementById('check-btn');
    const container = document.getElementById('claims-container');
    const status = document.getElementById('status');   
    button.disabled = true;
    container.innerHTML = '<div class="loading">Extracting claims...</div>';
    status.textContent = '';    
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      await chrome.tabs.sendMessage(tab.id, { action: 'removeHighlights' });
    } catch (error) {
    }   
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      const response = await chrome.tabs.sendMessage(tab.id, { action: 'extractClaims' });    
      if (!response || !response.claims || response.claims.length === 0) {
        container.innerHTML = '<div class="no-claims">No claims found on this page.</div>';
        button.disabled = false;
        return;
      }
      status.textContent = Found ${response.claims.length} claims. Checking...;
      container.innerHTML = '';      
      for (let i = 0; i < response.claims.length; i++) {
        const claim = response.claims[i];
        const claimId = claim-${++claimCounter};
        status.textContent = Checking claim ${i + 1} of ${response.claims.length}...;        
        try {
          await chrome.tabs.sendMessage(tab.id, { 
            action: 'highlightClaim', 
            claim, 
            claimId 
          });
        } catch (error) {
        }      
        const claimDiv = document.createElement('div');
        claimDiv.className = 'claim-item';
        claimDiv.id = sidepanel-${claimId};
        claimDiv.innerHTML = 
          <div class="claim-text">${escapeHtml(claim)}</div>
          <div class="loading">Verifying...</div>
        ;
        container.appendChild(claimDiv);
        chrome.runtime.sendMessage(
          { action: 'checkClaim', claim },
          (checkResponse) => {
            if (checkResponse.success) {
              const result = checkResponse.result;      
              const sourcesHtml = result.sources && result.sources.length > 0 ? 
                <div class="claim-sources">
                  ${result.sources.map(s => 
                    <div class="source-item">
                      <a href="${s.url}" target="_blank" class="source-link">
                        ${s.title || s.domain}
                      </a>
                      <div class="source-domain">
                        ${s.domain}
                        ${s.lastUpdated || s.date ?  • ${new Date(s.lastUpdated || s.date).toLocaleDateString()} : ''}
                      </div>
                      ${s.snippet ? <div class="source-snippet">${escapeHtml(s.snippet.substring(0, 150))}${s.snippet.length > 150 ? '...' : ''}</div> : ''}
                    </div>
                  ).join('')}
                </div> : 
                '<div class="claim-sources">No sources found</div>';             
              claimDiv.innerHTML = 
                <div class="claim-text">${escapeHtml(claim)}</div>
                ${sourcesHtml}
              ;          
              currentHighlights.set(claimId, claimDiv);
            } else {
              claimDiv.innerHTML = 
                <div class="claim-text">${escapeHtml(claim)}</div>
                <div class="error">Error: ${checkResponse.error}</div>
              ;
            }
          }
        );  
        if (i < response.claims.length - 1) {
          await new Promise(resolve => setTimeout(resolve, 500));
        }
      }  
      status.textContent = 'All claims checked! Click highlighted text on the page to jump to the corresponding claim details.';
      button.disabled = false; 
    } catch (error) {
      container.innerHTML = <div class="error">Error: ${error.message}</div>;
      button.disabled = false;
      status.textContent = '';
    }
  });  
  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

Zo werkt het zijpaneel in je browser:

  • Dynamische claimlijst: Wanneer de gebruiker op de knop “Check Claims” klikt, vraagt het script claims op van het actieve tabblad, wist oude highlights en maakt een lijst met claims met voorlopige statussen.
  • Realtime verificatie-updates: Elke claim wordt naar het backgroundscript gestuurd met chrome.runtime.sendMessage en de actie checkClaim. Wanneer de backend resultaten terugstuurt, werkt de UI direct bij met bronnen, domeinen en snippets.
  • Interactieve koppeling: Het script luistert naar berichten highlightClaimInPanel, zodat klikken op gemarkeerde tekst op de pagina naar de bijbehorende claimdetails in het zijpaneel scrollt. Dit zorgt voor vloeiende navigatie tussen paginahoogtepunten en verificatiegegevens.

Vervolgens voegen we een snelle pop-up toe om de extensie uit de vorige stappen makkelijker te starten en te gebruiken.

Stap 3.4: Snel toegankelijke pop-up-UI

Het bestand popup.js is een lichte pop-upinterface die verschijnt wanneer de gebruiker op het extensiepictogram klikt. In tegenstelling tot het zijpaneel biedt deze pop-up een snellere, on-demand manier om claims te checken zonder de huidige pagina te verlaten of een volledig interactief paneel te openen.

document.getElementById('check-btn').addEventListener('click', async () => {
    const button = document.getElementById('check-btn');
    const container = document.getElementById('claims-container');
    const status = document.getElementById('status');   
    button.disabled = true;
    container.innerHTML = '<div class="loading">Extracting claims...</div>';
    status.textContent = '';  
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });     
      const response = await chrome.tabs.sendMessage(tab.id, { action: 'extractClaims' });  
      if (!response || !response.claims || response.claims.length === 0) {
        container.innerHTML = '<div class="error">No claims found on this page.</div>';
        button.disabled = false;
        return;
      }   
      status.textContent = Found ${response.claims.length} claims. Checking...;
      container.innerHTML = '';
      for (let i = 0; i < response.claims.length; i++) {
        const claim = response.claims[i];
        status.textContent = Checking claim ${i + 1} of ${response.claims.length}...;   
        const claimDiv = document.createElement('div');
        claimDiv.className = 'claim-item';
        claimDiv.innerHTML = 
          <div class="claim-text">${escapeHtml(claim)}</div>
          <div class="loading">Verifying...</div>
        ;
        container.appendChild(claimDiv);      
        chrome.runtime.sendMessage(
          { action: 'checkClaim', claim },
          (checkResponse) => {
            if (checkResponse.success) {
              const result = checkResponse.result;
              const confidenceBar = result.confidence ? 
                <div class="confidence-bar">
                  <div class="confidence-fill" style="width: ${result.confidence}%"></div>
                  <span class="confidence-text">${result.confidence}% confidence</span>
                </div> : '';     
              const explanation = result.explanation ? 
                <div class="claim-explanation">${escapeHtml(result.explanation)}</div> : '';    
              const rebuttal = result.rebuttal ? 
                <div class="claim-rebuttal">
                  <strong>Counter-argument:</strong> ${escapeHtml(result.rebuttal)}
                </div> : '';  
              const sourcesHtml = result.sources && result.sources.length > 0 ? 
                <div class="claim-sources">
                  <strong>Sources (${result.sources.length}):</strong>
                  ${result.sources.map(s => 
                    <div class="source-item">
                      <a href="${s.url}" target="_blank" class="source-link">
                        ${s.domain || s.title}
                      </a>
                      ${s.snippet ? <div class="source-snippet">${escapeHtml(s.snippet)}</div> : ''}
                    </div>
                  ).join('')}
                </div> : 
                '<div class="claim-sources">No sources found</div>'; 
              claimDiv.innerHTML = 
                <div class="claim-text">${escapeHtml(claim)}</div>
                <div class="claim-verdict verdict-${result.verdict.toLowerCase()}">
                  ${result.verdict}
                </div>
                ${confidenceBar}
                ${explanation}
                ${rebuttal}
                ${sourcesHtml}
              ;
            } else {
              claimDiv.innerHTML = 
                <div class="claim-text">${escapeHtml(claim)}</div>
                <div class="error">Error: ${checkResponse.error}</div>
              ;
            }
          }
        );       
        if (i < response.claims.length - 1) {
          await new Promise(resolve => setTimeout(resolve, 500));
        }
      }  
      status.textContent = 'All claims checked!';
      button.disabled = false;      
    } catch (error) {
      container.innerHTML = <div class="error">Error: ${error.message}</div>;
      button.disabled = false;
      status.textContent = '';
    }
  }); 
  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

Zo zorgt het script popup.js voor een on-demand claimchecker direct vanuit de toolbar van de extensie.

  • Paginascan en claimweergave: Wanneer de gebruiker op de knop “Check” klikt, vraagt de pop-up via het contentscript tekst op van het actieve tabblad, extraheert snel mogelijke claims en toont deze in het pop-upvenster.
  • Backendverificatie: Elke claim wordt naar het backgroundscript gestuurd om de API-route /check-claim aan te roepen. De pop-up werkt realtime bij met uitspraken, betrouwbaarheidsbalken, toelichtingen, tegenargumenten en bronlinks die door de server worden geretourneerd.

Onze extensie is nu klaar om te testen. 

Opmerking: je vindt alle codebestanden voor dit project in de GitHub-repository.

Stap 4: De app uitvoeren

Start de backendserver met het volgende commando:

npm start

Zodra de server draait, laad je de Chrome-extensie:

  • Open Chrome en klik op “Extensies” (aan de rechterkant van de zoekbalk).
  • Zet “Ontwikkelaarsmodus” aan en klik op “Load unpacked” (linksboven). 
  • Laad vervolgens onze map met extensiebestanden en wacht op de bevestiging.

  • Nadat Chrome bevestigt dat de extensie is toegevoegd, klik je op het Claim Checker-pictogram op een willekeurige webpagina, selecteer je de tekst die je wilt verifiëren en voer je de claimcheck uit.

Laten we de resultaten bekijken:

Conclusie

Je hebt nu een snelle en betaalbare claimchecker die volledig wordt aangedreven door de Search API van Perplexity (geen LLM-calls nodig). Deze demo benadrukt de belangrijkste sterke punten van de API, zoals frisse en betrouwbare zoekresultaten, relevantie op snippetniveau, lage latentie en voorspelbare prijzen.

Vanaf hier kun je het project uitbreiden met geavanceerde filters, geautomatiseerde checks inplannen of het integreren als prefilter voor RAG-pijplijnen. Wil je verder gaan, bekijk dan de Perplexity API docs en quickstarts om meer geavanceerde zoek- en filtermogelijkheden te benutten.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Ik ben een Google Developers Expert in ML (Gen AI), een Kaggle 3x Expert en een Women Techmakers Ambassador met meer dan 3 jaar ervaring in tech. In 2020 heb ik een healthtech-startup mee opgericht en ik volg een master computer science aan Georgia Tech, met als specialisatie machine learning.

Onderwerpen
Kunstmatige intelligentie
AI Agents

Top DataCamp-cursussen

Cursus

Inleiding tot AI-agents

1 uur 30 min
142.5K
Leer de basis van AI-agents, hun onderdelen en echte toepassingen kennen — zonder dat je hoeft te coderen.
Bekijk detailsRight Arrow
Start Cursus
Meer bekijkenRight Arrow
Gerelateerd

blog

AI vanaf nul leren in 2026: een complete gids van de experts

Ontdek alles wat je moet weten om in 2026 AI te leren, van tips om te beginnen tot handige resources en inzichten van industrie-experts.
Adel Nehme's photo

Adel Nehme

15 min

Meer BekijkenMeer Bekijken