Skip to main content

Perplexity Search API Tutorial: Build a Real-Time Claim Checker in Your Browser

Learn how to use the Perplexity Search API to build a Chrome extension that verifies factual claims in real time. Includes Node.js backend setup and API integration guide.
Oct 15, 2025  · 10 min read

Perplexity recently released its Perplexity Search API, designed for fine-grained web retrieval that returns ranked, context-rich snippets instead of full pages. It’s built for real-time use cases, such as fact-checking, research, and RAG pipelines, where speed and precision matter.

In this tutorial, we’ll build Claim Checker, a browser extension that verifies factual statements on any webpage in real time (based on retrieved web evidence). You’ll see how the Perplexity Search API’s constantly refreshed index and low-latency retrieval stack, combined with semantic signals and multi-stage re-ranking, make it easy to find the most relevant evidence quickly. Along the way, you’ll learn how to:

  • Utilize the Perplexity Search API to fetch high-quality, ranked evidence from the web and process search results with custom sorting algorithms.
  • Develop a Node.js backend to securely manage your API key, handle CORS requests, and implement intelligent result processing with domain extraction and date-based ranking.
  • Create a Chrome/Edge extension with content scripts that enable text selection, side panel interfaces, and seamless communication between the browser and your backend server.
  • Display verification results, including source URLs, titles, snippets, domains, and publication dates in an interactive side panel with real-time claim processing.

By the end, you'll have a full-stack fact-checking tool that operates seamlessly within your browser. Users can select any text on a webpage and instantly get verification results with supporting sources, all while keeping their API credentials safe on the server side.

You can also check out our guide to Perplexity’s Comet Browser in a separate article. 

What Is Perplexity Search API?

The Perplexity Search API is a developer-friendly way to get fast and fine-grained web search results. 

Unlike traditional search APIs that return only full pages, Perplexity delivers ranked snippets and sub-document chunks containing the exact sentences and paragraphs that match your query. Some key features of this search API include:

  • Cost-efficient: The Search API is priced at $5 per 1,000 requests with no token-based billing, making it ideal for apps that run frequent or scheduled checks.
  • High-quality retrieval: It combines lexical and semantic ranking with multi-stage re-ranking, so you get highly relevant, up-to-date results from a massive, constantly refreshed index (~200M queries/day).
  • Low-latency: Perplexity search API returns queries quickly, keeping your UI responsive even when verifying multiple claims at once.

For our extension, this means we can pass any sentence as a query and get back reliable supporting or contradicting evidence, complete with titles, URLs, and context snippets, perfect for real-time fact-checking.

Perplexity Search API Project: Browser Claim Checker Extension

In this section, we’ll build a browser claim checker extension that instantly verifies factual claims using the Perplexity Search API. This project combines:

  • A lightweight Node.js backend that sends selected text to the Perplexity Search API and processes the results
  • A Chrome extension with a side panel interface to display verification details
  • A content script that captures user-selected text from any webpage

At the end of the project, your repository structure should look like this:

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

Let’s walk through each part of this project and see how they work together to deliver reliable claim verification inside the browser.

Step 1: Dependencies and libraries 

Before you begin, make sure the following prerequisites are in place:

  • A Perplexity account. Once you’ve signed up or signed in, navigate to the left-hand sidebar and expand the API section to access your API group and related settings.

  • Go to the API Billing tab in your API Portal and add your payment method. For this demo, about $10 in credits is sufficient to cover requests.
  • Node.js version 18 or above.
  • Chromium-based browser (Google Chrome or Microsoft Edge).

Next, install the required dependencies for the backend:

cd server
npm install express cors dotenv

Create a .env file in the server directory and add your Perplexity API key:

PERPLEXITY_API_KEY=your_api_key_here

Now we have all the dependencies installed.  Next, we can build our backend server and extension.

Step 2: Build the backend

The server.js file sets up a lightweight Node.js backend using Express. This backend acts as a bridge between the browser extension and the Perplexity Search API. It receives claims selected or entered by the user, queries Perplexity for relevant evidence, and returns a cleaned, structured response ready for display in the extension’s UI.

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');
});

The above code is the security and logic layer for the Claim Checker extension, which performs the following functions:

  • Secure and logical backend: It accepts a claim through the POST /check-claim route, validates the input, and calls the Perplexity Search API using a server-stored API key.
  • Processing and structuring: The server receives a list of relevant web sources and snippets, sorts them by recency using last_updated and date fields, selects the top three, and returns a clean JSON response for the extension.
  • Key safety: Running this backend keeps your API key hidden from the browser and allows you to add extra logic, such as ranking, filtering, or caching, before data reaches the user.
  • Extensible design: This approach makes the extension more secure and easily extensible for future improvements like advanced scoring, multi-source merging, or analytics.

Now we have our server ready. Let’s build a Chrome extension.

Step 3: Building the Chrome extension

In this step, we’ll build the Chrome extension that powers the user experience of Claim Checker. The extension is responsible for capturing claims from any webpage, sending them to our backend, and displaying the verification results in an intuitive interface.

Before diving into each file, here’s what these core components do:

  • background.js: This script runs as a service worker and manages background tasks such as listening for user actions, opening the side panel, and securely sending claims to the backend for verification.
  • content.js:  This content script runs inside the active webpage. It extracts the visible article text, identifies potential factual claims, and highlights them dynamically so the user can see what’s being checked.
  • sidepanel.js: This controls the interactive side panel UI that shows the extracted claims and their verification status. It updates results in real time and links them to the highlights on the page.
  • popup.js: This script powers the quick-access popup when the user clicks the extension icon. It provides a lightweight way to check claims without opening the side panel.

You can find a few extra helper files for the Chrome extension in the GitHub repository.

Step 3.1: Background script and event handling 

Building on our Chrome extension setup, the background.js file is the central controller that keeps the extension responsive. It listens for user actions (like clicking the extension icon), opens the side panel when needed, and forwards claims to the backend for verification.

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;
  }
}

The above code focuses on the responsiveness of the extension by incorporating the following:

  • Central event hub: The background script registers two main listeners, chrome.action.onClicked and chrome.runtime.onMessage. The first reacts when the user clicks the extension icon and tries to open the side panel. The second listens for custom messages like checkClaim coming from the content script or UI components.
  • Secure API handling: The checkClaim() function uses the fetch API to send the selected claim to the backend at http://localhost:3000/check-claim, because the API key is stored server-side, the browser never exposes it, keeping sensitive credentials secure.
  • Error resilience: Both event listeners include try/catch blocks to handle runtime errors. If the side panel cannot be opened, the script falls back to setting the pop-up. When API calls fail, it logs and returns structured error messages to the UI so users aren’t left with silent failures.

With the background script handling communication and secure API calls, the next step is to interact with the webpage itself.

Step 3.2: Extracting and highlighting claims

The content.js file runs directly inside the webpage and is responsible for extracting article text, identifying potential factual claims, and visually highlighting them for the user. It bridges the gap between the page content and the extension’s background logic by capturing relevant text and enabling user interaction.

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;
});

Let’s break down the key parts of the content.js script to understand how it extracts meaningful text, detects potential claims, and connects the highlighted content to the extension’s UI.

  • Article text extraction: The extractArticleText() function scans common article containers to pull the main readable content. This helps ensure the extension focuses on meaningful text instead of navigation menus or ads.
  • Claim detection logic: The extractClaims() function breaks text into sentences, applies filters for length and structure, and uses regex-based keywords to detect statements likely to be factual claims.
  • Dynamic highlighting: The highlightTextOnPage() function locates the detected claim within the DOM and wraps it in a styled <span>. It adds hover and click interactions so users can visually link claims on the page to their verification results in the side panel.
  • Message handling: The chrome.runtime.onMessage listener coordinates actions like extracting claims, adding highlights, and clearing them when needed. This keeps the page and extension synchronized and ensures smooth user interaction.

With the content script handling text extraction and claim highlighting on the page, the next step is to present those claims and their verification results to the user. This is where the sidepanel.js interface comes in.

Step 3.3: Side panel UI

The sidepanel.js file powers the interactive side panel where users can view extracted claims and their verification results. It connects the content script and backend responses to a clean, scrollable interface inside the extension.

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;
  }

Here is how the side panel works on your browser:

  • Dynamic claim listing: When the user clicks the “Check Claims” button, the script requests claims from the active tab, clears old highlights, and creates a list of claims with placeholder states.
  • Real-time verification updates: Each claim is sent to the background script using chrome.runtime.sendMessage with the checkClaim action. When the backend returns results, the UI updates instantly with sources, domains, and snippets.
  • Interactive linking: The script listens for highlightClaimInPanel messages, so clicking highlighted text on the page scrolls to the corresponding claim details in the side panel. This creates a smooth navigation loop between page highlights and verification data.

Next, we’ll add a quick-access pop-up to make it easier to launch and use the extension built in the previous steps.

Step 3.4: Quick access popup UI

The popup.js is a lightweight popup interface that appears when the user clicks the extension icon. Unlike the side panel, this pop-up provides a faster, on-demand way to check claims without leaving the current page or opening a full interactive panel.

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;
  }

Here is how the popup.js script powers an on-demand claim checker directly from the extension’s toolbar.

  • Page scan and claim display: When the user clicks the “Check” button, the pop-up requests text from the active tab through the content script, quickly extracts potential claims, and lists them in the pop-up window.
  • Backend verification: Each claim is sent to the background script to call the /check-claim API route. The pop-up updates in real time with verdicts, confidence bars, explanations, counter-arguments, and source links returned by the server.

Now, our extension is ready to be tested. 

Note: You can find all code files for this project in the GitHub repository.

Step 4: Running the app

Start the backend server by running the following command:

npm start

Once the server is running, load the Chrome extension:

  • Open Chrome and click on “Extensions” (on the right side of the search bar).
  • Turn on the “Developer mode” and click on “Load unpacked” (upper left). 
  • Next, load our extensions folder containing all files and wait for the confirmation.

  • After Chrome confirms the extension has been added, click the Claim Checker icon on any webpage, select the text you want to verify, and run the claim check.

Let’s see the results:

Conclusion

You now have a fast and affordable claim checker powered entirely by Perplexity’s Search API (no LLM calls required). This demo highlights the API’s key strengths, such as fresh and reliable search results, snippet-level relevance, low latency, and predictable pricing.

From here, you can extend the project by adding advanced filters, scheduling automated checks, or integrating it as a pre-filter for RAG pipelines. To go further, explore the Perplexity API docs and quickstart guides to unlock more advanced search and filtering capabilities.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

I am a Google Developers Expert in ML(Gen AI), a Kaggle 3x Expert, and a Women Techmakers Ambassador with 3+ years of experience in tech. I co-founded a health-tech startup in 2020 and am pursuing a master's in computer science at Georgia Tech, specializing in machine learning.

Topics

Top DataCamp Courses

Course

Designing Agentic Systems with LangChain

3 hr
6.6K
Get to grips with the foundational components of LangChain agents and build custom chat agents.
See DetailsRight Arrow
Start Course
See MoreRight Arrow
Related

blog

Perplexity vs. ChatGPT: Which AI Assistant Fits Your Needs Best?

A practical guide to choosing the right AI assistant for research, creativity, and everyday tasks.
Vinod Chugani's photo

Vinod Chugani

8 min

Tutorial

Perplexity's Comet Browser: A Guide With Examples

Learn what Perplexity’s Comet browser is and how to use it through five practical examples you can try yourself.
François Aubry's photo

François Aubry

Tutorial

Perplexity Labs: A Guide With 5 Practical Examples

Learn about Perplexity Labs, its features for creating reports, dashboards, and web applications, and how to use it through practical examples.
Bex Tuychiev's photo

Bex Tuychiev

Tutorial

OpenAI Realtime API: A Guide With Examples

Learn how to build real-time AI applications with OpenAI's Realtime API. This tutorial covers WebSockets, Node.js setup, text/audio messaging, function calling, and deploying a React voice assistant demo.
François Aubry's photo

François Aubry

Tutorial

Qwen-Agent: A Guide With Demo Project

Learn how to use Qwen-Agent and Qwen3 to build a real-time webpage summarizer extension.
Aashi Dutt's photo

Aashi Dutt

Tutorial

Mistral Agents API: A Guide With Demo Project

Learn how to build AI agents using Mistral's Agents API, and explore key concepts like tool usage, connectors, handoffs, and more.
Aashi Dutt's photo

Aashi Dutt

See MoreSee More