Corso
Perplexity ha recentemente rilasciato la sua Perplexity Search API, pensata per un recupero web granulare che restituisce snippet classificati e ricchi di contesto invece di pagine intere. È costruita per casi d’uso in tempo reale, come fact-checking, ricerca e pipeline RAG, dove velocità e precisione sono fondamentali.
In questo tutorial costruiremo Claim Checker, un’estensione del browser che verifica in tempo reale le affermazioni fattuali su qualsiasi pagina web (basandosi su evidenze recuperate dal web). Vedrai come l’indice costantemente aggiornato della Perplexity Search API e lo stack di retrieval a bassa latenza, combinati con segnali semantici e riordinamento multi-stadio, rendano semplice trovare rapidamente le prove più pertinenti. Lungo il percorso imparerai a:
- Usare la Perplexity Search API per ottenere dal web evidenze di alta qualità e classificate, e processare i risultati di ricerca con algoritmi di ordinamento personalizzati.
- Sviluppare un backend Node.js per gestire in sicurezza la tua chiave API, gestire richieste CORS e implementare un’elaborazione intelligente dei risultati con estrazione del dominio e ranking basato sulle date.
- Creare un’estensione Chrome/Edge con content script che abilitano la selezione del testo, interfacce nel pannello laterale e una comunicazione fluida tra il browser e il tuo server backend.
- Mostrare i risultati della verifica, inclusi URL delle fonti, titoli, snippet, domini e date di pubblicazione in un pannello laterale interattivo con elaborazione delle affermazioni in tempo reale.
Alla fine, avrai uno strumento full-stack di fact-checking che funziona in modo trasparente nel tuo browser. Gli utenti possono selezionare qualsiasi testo su una pagina web e ottenere istantaneamente i risultati della verifica con le fonti di supporto, mantenendo al contempo le credenziali API al sicuro sul lato server.
Puoi anche dare un’occhiata alla nostra guida a Comet Browser di Perplexity in un articolo a parte.
Che cos’è la Perplexity Search API?
La Perplexity Search API è un modo pensato per gli sviluppatori per ottenere risultati di ricerca web rapidi e granulari.
A differenza delle API di ricerca tradizionali che restituiscono solo pagine intere, Perplexity fornisce snippet classificati e porzioni di documento con le frasi e i paragrafi che corrispondono esattamente alla tua query. Alcune caratteristiche chiave di questa API di ricerca includono:
- Conveniente: la Search API costa 5 $ ogni 1.000 richieste senza fatturazione a token, ideale per app che eseguono controlli frequenti o pianificati.
- Recupero di alta qualità: combina ranking lessicale e semantico con riordinamento multi-stadio, così ottieni risultati estremamente pertinenti e aggiornati da un enorme indice costantemente rinfrescato (~200M query/giorno).
- Bassa latenza: la Perplexity Search API risponde rapidamente, mantenendo la tua UI reattiva anche quando verifichi più affermazioni in parallelo.
Per la nostra estensione, questo significa che possiamo passare qualsiasi frase come query e ottenere evidenze affidabili a supporto o confutazione, complete di titoli, URL e snippet di contesto, perfette per il fact-checking in tempo reale.
Progetto Perplexity Search API: estensione Browser Claim Checker
In questa sezione costruiremo un’estensione del browser per il controllo delle affermazioni che verifica istantaneamente le affermazioni fattuali usando la Perplexity Search API. Questo progetto combina:
- Un backend Node.js leggero che invia il testo selezionato alla Perplexity Search API ed elabora i risultati
- Un’estensione Chrome con un’interfaccia nel pannello laterale per mostrare i dettagli della verifica
- Un content script che cattura il testo selezionato dall’utente su qualsiasi pagina web
Alla fine del progetto, la struttura del tuo repository dovrebbe essere così:
ClaimChecker/
├── server/
│ ├── server.js
│ ├── package.json
│ └── .env
├── extension/
│ ├── manifest.json
│ ├── background.js
│ ├── content.js
│ ├── sidepanel.html
│ ├── sidepanel.js
│ ├── popup.html
│ └── popup.js
└── README.md
Vediamo ogni parte del progetto e come lavorano insieme per offrire una verifica affidabile delle affermazioni dentro il browser.
Step 1: Dipendenze e librerie
Prima di iniziare, assicurati di avere i seguenti prerequisiti:
- Un account Perplexity. Dopo esserti registrato o aver effettuato l’accesso, vai nella barra laterale sinistra ed espandi la sezione API per accedere al tuo gruppo API e alle impostazioni correlate.

- Vai alla scheda API Billing nel tuo API Portal e aggiungi un metodo di pagamento. Per questa demo, circa 10 $ di credito bastano a coprire le richieste.
- Node.js versione 18 o superiore.
- Browser basato su Chromium (Google Chrome o Microsoft Edge).
Ora installa le dipendenze richieste per il backend:
cd server
npm install express cors dotenv
Crea un file .env nella directory server e aggiungi la tua chiave API Perplexity:
PERPLEXITY_API_KEY=your_api_key_here
Ora abbiamo installato tutte le dipendenze. Possiamo passare a costruire il nostro server backend e l’estensione.
Step 2: Crea il backend
Il file server.js configura un backend Node.js leggero usando Express. Questo backend fa da ponte tra l’estensione del browser e la Perplexity Search API. Riceve affermazioni selezionate o inserite dall’utente, interroga Perplexity per evidenze pertinenti e restituisce una risposta pulita e strutturata pronta per essere mostrata nella UI dell’estensione.
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');
});
Il codice sopra è lo strato di sicurezza e logica per l’estensione Claim Checker, che svolge le seguenti funzioni:
- Backend sicuro e logico: accetta un’affermazione tramite la rotta
POST /check-claim, valida l’input e chiama la Perplexity Search API usando una chiave API archiviata sul server. - Elaborazione e strutturazione: il server riceve un elenco di fonti web e snippet pertinenti, li ordina per recentità usando i campi
last_updatede data, seleziona i primi tre e restituisce una risposta JSON pulita per l’estensione. - Sicurezza delle chiavi: eseguire questo backend mantiene la tua chiave API nascosta dal browser e ti permette di aggiungere logica extra, come ranking, filtraggio o caching, prima che i dati arrivino all’utente.
- Design estendibile: questo approccio rende l’estensione più sicura e facilmente estendibile per futuri miglioramenti come scoring avanzato, fusione multi-fonte o analytics.
Ora che il server è pronto, costruiamo un’estensione Chrome.
Step 3: Creare l’estensione Chrome
In questo step costruiremo l’estensione Chrome che alimenta l’esperienza utente di Claim Checker. L’estensione si occupa di catturare affermazioni da qualsiasi pagina web, inviarle al nostro backend e mostrare i risultati della verifica in un’interfaccia intuitiva.
Prima di entrare nei dettagli di ogni file, ecco cosa fanno questi componenti principali:
background.js: questo script gira come service worker e gestisce attività in background come l’ascolto delle azioni dell’utente, l’apertura del pannello laterale e l’invio sicuro delle affermazioni al backend per la verifica.content.js: questo content script gira all’interno della pagina attiva. Estrae il testo visibile dell’articolo, identifica possibili affermazioni fattuali e le evidenzia dinamicamente così l’utente può vedere cosa viene controllato.sidepanel.js: controlla la UI interattiva del pannello laterale che mostra le affermazioni estratte e lo stato della loro verifica. Aggiorna i risultati in tempo reale e li collega alle evidenziazioni sulla pagina.popup.js: alimenta il popup di accesso rapido quando l’utente clicca sull’icona dell’estensione. Offre un modo leggero per controllare le affermazioni senza aprire il pannello laterale.
Puoi trovare alcuni file helper aggiuntivi per l’estensione Chrome nel repository GitHub.
Step 3.1: Background script e gestione eventi
Partendo dal setup dell’estensione Chrome, il file background.js è il controller centrale che mantiene l’estensione reattiva. Ascolta le azioni dell’utente (come il clic sull’icona dell’estensione), apre il pannello laterale quando serve e inoltra le affermazioni al backend per la verifica.
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;
}
}
Il codice sopra punta sulla reattività dell’estensione grazie a:
- Hub centrale degli eventi: lo script background registra due listener principali,
chrome.action.onClickedechrome.runtime.onMessage. Il primo reagisce al clic sull’icona dell’estensione e prova ad aprire il pannello laterale. Il secondo ascolta messaggi personalizzati comecheckClaimprovenienti dal content script o dai componenti UI. - Gestione sicura dell’API: la funzione
checkClaim()usa lafetchAPI per inviare l’affermazione selezionata al backend suhttp://localhost:3000/check-claim; poiché la chiave API è conservata lato server, il browser non la espone mai, mantenendo al sicuro le credenziali sensibili. - Resilienza agli errori: entrambi i listener includono blocchi
try/catchper gestire errori a runtime. Se non è possibile aprire il pannello laterale, lo script ripiega impostando il popup. Quando le chiamate API falliscono, registra e restituisce messaggi di errore strutturati alla UI così gli utenti non restano senza feedback.
Con lo script background che gestisce la comunicazione e le chiamate API in modo sicuro, il prossimo passo è interagire con la pagina web stessa.
Step 3.2: Estrazione ed evidenziazione delle affermazioni
Il file content.js gira direttamente dentro la pagina web e si occupa di estrarre il testo dell’articolo, identificare possibili affermazioni fattuali e metterle in evidenza visivamente per l’utente. Colma il divario tra il contenuto della pagina e la logica in background dell’estensione catturando il testo rilevante e abilitando l’interazione utente.
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;
});
Vediamo le parti chiave dello script content.js per capire come estrae testo significativo, rileva possibili affermazioni e collega il contenuto evidenziato alla UI dell’estensione.
- Estrazione del testo dell’articolo: la funzione
extractArticleText()scansiona i contenitori comuni degli articoli per recuperare il contenuto principale leggibile. Questo aiuta a focalizzarsi su testo significativo anziché su menu di navigazione o annunci. - Logica di rilevamento delle affermazioni: la funzione
extractClaims()suddivide il testo in frasi, applica filtri per lunghezza e struttura e usa parole chiave basate su regex per individuare enunciati che con ogni probabilità sono affermazioni fattuali. - Evidenziazione dinamica: la funzione
highlightTextOnPage()individua l’affermazione rilevata nel DOM e la racchiude in uno<span>stilizzato. Aggiunge interazioni hover e click per collegare visivamente le affermazioni sulla pagina ai loro risultati di verifica nel pannello laterale. - Gestione dei messaggi: il listener
chrome.runtime.onMessagecoordina azioni come estrarre affermazioni, aggiungere evidenziazioni e rimuoverle quando necessario. Mantiene sincronizzate pagina ed estensione e assicura un’interazione fluida.
Con il content script che gestisce l’estrazione del testo e l’evidenziazione delle affermazioni sulla pagina, il passo successivo è presentare tali affermazioni e i relativi risultati di verifica all’utente. Qui entra in gioco l’interfaccia sidepanel.js.
Step 3.3: UI del pannello laterale
Il file sidepanel.js alimenta il pannello laterale interattivo in cui gli utenti possono vedere le affermazioni estratte e i loro risultati di verifica. Collega il content script e le risposte del backend a un’interfaccia pulita e scorrevole dentro l’estensione.
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;
}
Ecco come funziona il pannello laterale nel tuo browser:
- Elenco dinamico delle affermazioni: quando l’utente clicca il pulsante “Check Claims”, lo script richiede le affermazioni dalla scheda attiva, pulisce le vecchie evidenziazioni e crea un elenco di affermazioni con stati placeholder.
- Aggiornamenti di verifica in tempo reale: ogni affermazione viene inviata allo script background usando
chrome.runtime.sendMessagecon l’azionecheckClaim. Quando il backend restituisce i risultati, la UI si aggiorna istantaneamente con fonti, domini e snippet. - Collegamento interattivo: lo script ascolta i messaggi
highlightClaimInPanel, così cliccando il testo evidenziato sulla pagina si scorre fino ai dettagli corrispondenti nel pannello laterale. Questo crea un loop di navigazione fluido tra evidenziazioni e dati di verifica.
Ora aggiungeremo un pop-up di accesso rapido per semplificare l’avvio e l’uso dell’estensione costruita negli step precedenti.
Step 3.4: Popup di accesso rapido
Il popup.js è un’interfaccia popup leggera che appare quando l’utente clicca l’icona dell’estensione. A differenza del pannello laterale, questo pop-up offre un modo più rapido e on-demand per controllare le affermazioni senza lasciare la pagina corrente o aprire un pannello interattivo completo.
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;
}
Ecco come lo script popup.js fornisce un verificatore on-demand direttamente dalla toolbar dell’estensione.
- Scansione della pagina e visualizzazione delle affermazioni: quando l’utente clicca il pulsante “Check”, il pop-up richiede il testo dalla scheda attiva tramite il content script, estrae rapidamente le possibili affermazioni e le elenca nella finestra del pop-up.
- Verifica lato backend: ogni affermazione è inviata allo script background per chiamare la rotta
/check-claim. Il pop-up si aggiorna in tempo reale con verdetti, barre di confidenza, spiegazioni, controargomentazioni e link alle fonti restituiti dal server.
A questo punto, la nostra estensione è pronta per essere testata.
Nota: puoi trovare tutti i file di codice per questo progetto nel repository GitHub.
Step 4: Esecuzione dell’app
Avvia il server backend eseguendo il seguente comando:
npm start
Una volta che il server è in esecuzione, carica l’estensione Chrome:
- Apri Chrome e clicca su “Extensions” (sul lato destro della barra di ricerca).
- Attiva la “Modalità sviluppatore” e clicca su “Load unpacked” (in alto a sinistra).
- Quindi carica la cartella dell’estensione contenente tutti i file e attendi la conferma.

- Dopo che Chrome conferma che l’estensione è stata aggiunta, clicca sull’icona di Claim Checker su qualsiasi pagina web, seleziona il testo che vuoi verificare ed esegui il controllo.
Vediamo i risultati:

Conclusione
Ora hai un verificatore di affermazioni veloce e conveniente, alimentato interamente dalla Search API di Perplexity (nessuna chiamata a LLM necessaria). Questa demo mette in evidenza i punti di forza dell’API, come risultati di ricerca freschi e affidabili, rilevanza a livello di snippet, bassa latenza e prezzi prevedibili.
Da qui, puoi estendere il progetto aggiungendo filtri avanzati, pianificando controlli automatici o integrandolo come pre-filtro per pipeline RAG. Per approfondire, esplora la Perplexity API docs e le guide quickstart per sbloccare funzionalità di ricerca e filtraggio più avanzate.
Sono una Google Developers Expert in ML (Gen AI), una Kaggle 3x Expert e una Women Techmakers Ambassador con oltre 3 anni di esperienza nel tech. Ho co-fondato una startup health-tech nel 2020 e sto conseguendo un master in informatica al Georgia Tech, con specializzazione in machine learning.


