Curso
Perplexity ha lanzado recientemente su Perplexity Search API, diseñada para recuperar información web con gran nivel de detalle y devolver fragmentos clasificados y con contexto en lugar de páginas completas. Está pensada para casos de uso en tiempo real, como verificación de hechos, investigación y canalizaciones RAG, donde la velocidad y la precisión importan.
En este tutorial, construiremos Claim Checker, una extensión de navegador que verifica en tiempo real las afirmaciones de cualquier página web (basándose en evidencias recuperadas de la web). Verás cómo el índice constantemente actualizado de la Perplexity Search API y su pila de recuperación de baja latencia, combinados con señales semánticas y reordenación en varias etapas, facilitan encontrar rápidamente la evidencia más relevante. Por el camino, aprenderás a:
- Usar la Perplexity Search API para obtener evidencia de calidad de la web y procesar los resultados de búsqueda con algoritmos de ordenación personalizados.
- Desarrollar un backend en Node.js para gestionar de forma segura tu clave de API, manejar solicitudes CORS e implementar un procesamiento inteligente de resultados con extracción de dominio y ordenación por fecha.
- Crear una extensión para Chrome/Edge con scripts de contenido que permitan seleccionar texto, mostrar un panel lateral e integrar la comunicación entre el navegador y tu servidor backend sin fricciones.
- Mostrar los resultados de verificación —incluyendo URLs de origen, títulos, fragmentos, dominios y fechas de publicación— en un panel lateral interactivo con procesamiento de afirmaciones en tiempo real.
Al final tendrás una herramienta full‑stack de verificación que funciona de forma fluida dentro de tu navegador. Los usuarios podrán seleccionar cualquier texto en una página y obtener al instante resultados de verificación con fuentes de apoyo, manteniendo las credenciales de la API seguras en el servidor.
También puedes consultar nuestra guía del navegador Comet de Perplexity en otro artículo.
¿Qué es la Perplexity Search API?
La Perplexity Search API ofrece a desarrolladores una forma ágil de obtener resultados de búsqueda web rápidos y granulares.
A diferencia de las APIs de búsqueda tradicionales que devuelven páginas completas, Perplexity entrega fragmentos clasificados y trozos de subdocumentos con las frases y párrafos exactos que coinciden con tu consulta. Algunas características clave de esta API de búsqueda son:
- Eficiente en costes: precio de 5 $ por 1.000 solicitudes sin facturación por tokens, ideal para apps con comprobaciones frecuentes o programadas.
- Recuperación de alta calidad: combina ranking léxico y semántico con reordenación en varias etapas para ofrecer resultados muy relevantes y actualizados desde un índice masivo y en continua actualización (~200 M de consultas/día).
- Baja latencia: la Search API de Perplexity responde rápido, manteniendo tu interfaz fluida incluso al verificar varias afirmaciones a la vez.
Para nuestra extensión, esto significa que podemos pasar cualquier frase como consulta y obtener evidencia fiable a favor o en contra, con títulos, URLs y fragmentos de contexto: perfecto para verificación de hechos en tiempo real.
Proyecto con Perplexity Search API: extensión de verificación de afirmaciones en el navegador
En esta sección construiremos una extensión que verifica al instante afirmaciones factuales usando la Perplexity Search API. Este proyecto combina:
- Un backend ligero en Node.js que envía el texto seleccionado a la Perplexity Search API y procesa los resultados
- Una extensión de Chrome con un panel lateral para mostrar los detalles de verificación
- Un script de contenido que captura el texto seleccionado por el usuario en cualquier página
Al final del proyecto, la estructura de tu repositorio debería verse así:
ClaimChecker/
├── server/
│ ├── server.js
│ ├── package.json
│ └── .env
├── extension/
│ ├── manifest.json
│ ├── background.js
│ ├── content.js
│ ├── sidepanel.html
│ ├── sidepanel.js
│ ├── popup.html
│ └── popup.js
└── README.md
Vamos a recorrer cada parte del proyecto y ver cómo se combinan para ofrecer verificación fiable dentro del navegador.
Paso 1: dependencias y librerías
Antes de empezar, asegúrate de cumplir estos requisitos:
- Una cuenta de Perplexity. Tras registrarte o iniciar sesión, ve a la barra lateral izquierda y despliega la sección API para acceder a tu grupo de API y sus ajustes.

- Ve a la pestaña API Billing de tu portal de API y añade tu método de pago. Para esta demo, unos 10 $ en créditos serán suficientes para cubrir las solicitudes.
- Node.js versión 18 o superior.
- Navegador basado en Chromium (Google Chrome o Microsoft Edge).
Después, instala las dependencias necesarias para el backend:
cd server
npm install express cors dotenv
Crea un archivo .env en el directorio del servidor y añade tu clave de la API de Perplexity:
PERPLEXITY_API_KEY=your_api_key_here
Ya tenemos todas las dependencias instaladas. Ahora podemos crear el servidor backend y la extensión.
Paso 2: crea el backend
El archivo server.js configura un backend ligero en Node.js usando Express. Este backend actúa como puente entre la extensión y la Perplexity Search API. Recibe las afirmaciones seleccionadas o introducidas por el usuario, consulta a Perplexity para encontrar evidencia relevante y devuelve una respuesta limpia y estructurada lista para mostrarse en la interfaz de la extensión.
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');
});
El código anterior es la capa de seguridad y lógica de la extensión Claim Checker y realiza lo siguiente:
- Backend seguro y lógico: acepta una afirmación mediante la ruta
POST /check-claim, valida la entrada y llama a la Perplexity Search API usando una clave de API almacenada en el servidor. - Procesamiento y estructuración: recibe una lista de fuentes y fragmentos relevantes, los ordena por recencia usando
last_updatedy la fecha, selecciona los tres mejores y devuelve un JSON limpio para la extensión. - Protección de claves: al ejecutar este backend, tu clave de API permanece oculta para el navegador y puedes añadir lógica extra —ranking, filtrado o caché— antes de que los datos lleguen al usuario.
- Diseño extensible: este enfoque hace la extensión más segura y fácil de ampliar con mejoras futuras como puntuación avanzada, combinación de múltiples fuentes o analítica.
Con el servidor listo, vamos a crear la extensión para Chrome.
Paso 3: crea la extensión de Chrome
En este paso construiremos la extensión que da forma a la experiencia de Claim Checker. La extensión captura afirmaciones en cualquier página, las envía a nuestro backend y muestra los resultados de verificación en una interfaz intuitiva.
Antes de entrar en cada archivo, esto es lo que hace cada componente principal:
background.js: se ejecuta como service worker y gestiona tareas en segundo plano: escucha acciones del usuario, abre el panel lateral y envía las afirmaciones al backend de forma segura para su verificación.content.js: este script se ejecuta dentro de la página activa. Extrae el texto del artículo visible, identifica posibles afirmaciones y las resalta dinámicamente para que el usuario vea qué se está comprobando.sidepanel.js: controla la interfaz del panel lateral que muestra las afirmaciones extraídas y su estado de verificación. Actualiza resultados en tiempo real y los enlaza con los resaltados en la página.popup.js: alimenta el popup de acceso rápido al hacer clic en el icono de la extensión. Permite comprobar afirmaciones sin abrir el panel lateral.
Puedes encontrar algunos archivos auxiliares para la extensión de Chrome en el repositorio de GitHub.
Paso 3.1: script en segundo plano y manejo de eventos
Partiendo de la configuración de la extensión, el archivo background.js es el controlador central que mantiene la extensión reactiva. Escucha acciones del usuario (como hacer clic en el icono), abre el panel lateral cuando hace falta y reenvía las afirmaciones al backend para su verificación.
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;
}
}
Este código se centra en la reactividad de la extensión e incluye lo siguiente:
- Centro de eventos: el script de fondo registra dos listeners principales,
chrome.action.onClickedychrome.runtime.onMessage. El primero reacciona al clic en el icono e intenta abrir el panel lateral. El segundo escucha mensajes comocheckClaimdesde el script de contenido o la UI. - Gestión segura de la API: la función
checkClaim()usafetchpara enviar la afirmación seleccionada al backend enhttp://localhost:3000/check-claim. Como la clave de API está en el servidor, el navegador nunca la expone y las credenciales quedan protegidas. - Resiliencia ante errores: ambos listeners incluyen bloques
try/catchpara gestionar errores en tiempo de ejecución. Si no puede abrirse el panel lateral, recurre al popup. Cuando fallan las llamadas a la API, registra y devuelve errores estructurados a la UI para evitar fallos silenciosos.
Con el script de fondo gestionando la comunicación y las llamadas seguras, el siguiente paso es interactuar con la propia página web.
Paso 3.2: extracción y resaltado de afirmaciones
El archivo content.js se ejecuta dentro de la página y se encarga de extraer el texto del artículo, detectar posibles afirmaciones y resaltarlas visualmente para el usuario. Conecta el contenido de la página con la lógica de la extensión capturando texto relevante y habilitando la interacción.
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;
});
Desgranemos las partes clave del script content.js para entender cómo extrae texto significativo, detecta posibles afirmaciones y conecta el contenido resaltado con la interfaz de la extensión.
- Extracción del texto del artículo: la función
extractArticleText()analiza contenedores comunes de artículos para extraer el contenido legible principal y evitar menús de navegación o anuncios. - Lógica de detección de afirmaciones:
extractClaims()divide el texto en frases, aplica filtros de longitud y estructura y usa expresiones regulares para detectar enunciados que probablemente sean afirmaciones factuales. - Resaltado dinámico:
highlightTextOnPage()localiza la afirmación en el DOM y la envuelve en un<span>con estilos. Añade interacciones de hover y clic para vincular visualmente las afirmaciones con sus resultados en el panel lateral. - Gestión de mensajes: el listener
chrome.runtime.onMessagecoordina acciones como extraer afirmaciones, añadir resaltados o limpiarlos cuando sea necesario. Mantiene sincronizados la página y la extensión para una experiencia fluida.
Con el script de contenido ocupándose de la extracción y el resaltado, el siguiente paso es presentar las afirmaciones y sus resultados de verificación. Aquí entra la interfaz de sidepanel.js.
Paso 3.3: interfaz del panel lateral
El archivo sidepanel.js alimenta el panel lateral interactivo donde el usuario ve las afirmaciones extraídas y sus resultados. Conecta el script de contenido y las respuestas del backend en una interfaz limpia y desplazable dentro de la extensión.
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;
}
Así funciona el panel lateral en tu navegador:
- Listado dinámico de afirmaciones: al pulsar el botón “Check Claims”, el script solicita las afirmaciones de la pestaña activa, limpia los resaltados antiguos y crea una lista con estados provisionales.
- Actualizaciones en tiempo real: cada afirmación se envía al script de fondo con la acción
checkClaim. Cuando el backend devuelve resultados, la UI se actualiza al instante con fuentes, dominios y fragmentos. - Enlace interactivo: el script escucha mensajes
highlightClaimInPanel, de modo que al hacer clic en el texto resaltado de la página, el panel lateral se desplaza al detalle correspondiente. Navegación fluida entre resaltados y verificación.
A continuación, añadiremos un popup de acceso rápido para facilitar el uso de la extensión creada en los pasos anteriores.
Paso 3.4: popup de acceso rápido
El popup.js es una interfaz ligera que aparece al hacer clic en el icono de la extensión. A diferencia del panel lateral, este popup ofrece una forma rápida y bajo demanda de comprobar afirmaciones sin abrir un panel 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;
}
Así es como popup.js ofrece un verificador bajo demanda directamente desde la barra de herramientas.
- Escaneo de página y listado: al pulsar “Check”, el popup solicita texto a la pestaña activa a través del script de contenido, extrae rápidamente posibles afirmaciones y las lista en la ventana del popup.
- Verificación en el backend: cada afirmación se envía al script de fondo para llamar a la ruta
/check-claim. El popup se actualiza en tiempo real con veredictos, barras de confianza, explicaciones, contraargumentos y enlaces a fuentes que devuelve el servidor.
Ya podemos probar la extensión.
Nota: puedes encontrar todos los archivos de código del proyecto en el repositorio de GitHub.
Paso 4: ejecución de la app
Inicia el servidor backend con el siguiente comando:
npm start
Cuando el servidor esté en marcha, carga la extensión de Chrome:
- Abre Chrome y haz clic en “Extensions” (a la derecha de la barra de búsqueda).
- Activa el “Developer mode” y haz clic en “Load unpacked” (arriba a la izquierda).
- Luego, carga la carpeta de la extensión con todos los archivos y espera la confirmación.

- Cuando Chrome confirme que se ha añadido, haz clic en el icono de Claim Checker en cualquier página, selecciona el texto que quieras verificar y ejecuta la comprobación.
Veamos los resultados:

Conclusión
Ya tienes un verificador de afirmaciones rápido y asequible, impulsado íntegramente por la Search API de Perplexity (sin llamadas a LLM). Esta demo destaca sus puntos fuertes: resultados frescos y fiables, relevancia a nivel de fragmento, baja latencia y precios predecibles.
A partir de aquí, puedes ampliar el proyecto con filtros avanzados, programar comprobaciones automáticas o integrarlo como prefiltro en canalizaciones RAG. Para ir más allá, explora la Perplexity API docs y las guías de inicio rápido para desbloquear funciones avanzadas de búsqueda y filtrado.
Soy experta Google Developers en ML (Gen AI), triple experta en Kaggle y embajadora de Women Techmakers, con más de tres años de experiencia en el sector tecnológico. Cofundé una startup de salud en 2020 y actualmente curso un máster en informática en Georgia Tech, con especialización en aprendizaje automático.
