Lo esencial
Eso es un agente de navegador: un sistema de IA que controla un navegador real, lee las páginas como lo haría una persona y decide la siguiente acción según lo que ve. No es un simple script con selectores CSS fijos, sino algo que entiende el sentido de la página.
En esta lección vemos cómo construir estos agentes con Browserbase y Stagehand, en qué se diferencian de la automatización clásica, y escribimos un agente real que monitorea los precios de la competencia.
Conceptos clave
- Agente de navegador vs. automatización clásica: la IA entiende el sentido de la página, no solo busca selectores CSS
- Browserbase: infraestructura de navegadores en la nube para agentes de IA, con funciones antidetección
- Stagehand: SDK de Browserbase sobre Playwright con los métodos
act(),extract()yobserve(), controlados por un LLM - Session replay: grabar y reproducir sesiones para depurar agentes
- Técnicas antidetección: modo stealth, proxies residenciales, resolución de CAPTCHA
- Ética del scraping: datos públicos vs. privados, Terms of Service, datos personales (PII)
- Monetización: servicios de recolección y monitoreo de datos (un formato de servicio, no una garantía de ingresos)
Teoría
Por qué los agentes de navegador no son Selenium 2.0
Cuando apareció Selenium en 2004, la automatización web se construía con un principio: el programador encuentra a mano el selector CSS o la ruta XPath del elemento y le dice al script: "haz clic en #buy-button". Funcionaba mientras los sitios no cambiaran su maquetación. Pero bastaba con que los desarrolladores cambiaran la clase del botón para que el script se rompiera.
Puppeteer (2017) y Playwright (2020) hicieron ese enfoque más rápido y confiable, pero el principio siguió igual: instrucciones rígidas basadas en la estructura del DOM. Si la página cambia, la automatización se rompe.
Un agente de navegador piensa distinto. En vez de "haz clic en el elemento con id=submit", recibe la tarea "haz el pedido" y él mismo resuelve qué hay que hacer. Lee el texto de la página, entiende el contexto, ve que el botón dice "Finalizar compra" o "Checkout" y le da clic. Si la maquetación cambió, el agente simplemente rehace su plan.
Browserbase: un navegador en la nube para la IA
Browserbase es una infraestructura administrada en la nube para ejecutar navegadores. No es solo un Chrome headless en tu servidor, sino una plataforma completa con varias capacidades clave:
Qué te da Browserbase:
Navegadores administrados: no tienes que configurar Chromium, versiones ni dependencias. Browserbase corre los navegadores en su infraestructura y tú te conectas por WebSocket.
Session Replay: cada sesión del agente se puede reproducir como video. Indispensable para depurar: ves literalmente lo que vio el agente en el momento del error.
Modo stealth: los navegadores están configurados para parecer un usuario común: encabezados correctos, una huella de navegador realista. Con eso muchas veces pasan las revisiones antibot básicas; las protecciones serias, no siempre.
Paralelismo: puedes correr muchas sesiones a la vez (el límite depende del plan). Monitorear decenas de sitios al mismo tiempo es una tarea común.
Proxies residenciales: la opción de salir a internet por direcciones IP reales de usuarios de distintos países.
Precios: hay un plan gratuito con pocas horas de navegador al mes y planes de pago con más horas y más navegadores simultáneos. Para probar una idea alcanza el plan gratuito. Planes y precios vigentes: browserbase.com/pricing.
Stagehand: un Playwright con IA
Stagehand es un SDK de código abierto del equipo de Browserbase que le agrega inteligencia artificial a la automatización del navegador (originalmente sobre Playwright). Si Playwright son las manos (las herramientas para controlar el navegador), Stagehand es el cerebro que decide qué hacer con las manos.
Sobre las versiones. En la versión 3, los métodos act(), extract() y observe() se llaman en el propio objeto stagehand, y la página se toma de stagehand.context.pages()[0]. En la versión 2 se llamaban en page (page.act(...)). Los ejemplos de abajo están escritos para la versión 3; antes de empezar, revisa la documentación de la versión que instalaste: docs.stagehand.dev.
Tres métodos clave:
act(instruction): ejecuta una acción
await stagehand.act('Haz clic en el botón "Iniciar sesión"');
await stagehand.act('Llena el campo email con [email protected]');
await stagehand.act('Desplázate hacia abajo hasta la sección "Precios"');Stagehand toma tu instrucción en lenguaje natural, mira la página con una captura o con el DOM, identifica el elemento y ejecuta la acción. Si el elemento se movió, no hay problema: la IA lo vuelve a encontrar.
extract(instruction, schema): extrae datos
const products = await stagehand.extract(
'Encuentra todos los productos con precio en la página',
z.object({
items: z.array(z.object({
name: z.string(),
price: z.number(),
inStock: z.boolean(),
}))
})
);El método devuelve datos estructurados (validados con Zod) extraídos de la página. La IA entiende qué es "precio" y "disponibilidad" aunque cada sitio lo muestre de forma distinta.
observe(instruction): mira y dime
const elements = await stagehand.observe(
'¿Qué botones hay disponibles para acciones con este producto?'
);
// devuelve una lista de elementos encontrados y acciones sugeridas,
// por ejemplo los botones "Agregar al carrito", "Comprar ahora", "Comparar"Observar sin actuar: útil para tomar decisiones dentro del ciclo del agente.
Ejemplo completo: monitorear los precios de la competencia
import { Stagehand } from '@browserbasehq/stagehand';
import { z } from 'zod';
const PriceSchema = z.object({
products: z.array(z.object({
name: z.string().describe('Nombre del producto'),
price: z.number().describe('Precio en la moneda de la tienda'),
oldPrice: z.number().nullable().describe('Precio anterior, si hay descuento'),
inStock: z.boolean().describe('Si hay existencias'),
url: z.string().describe('Enlace al producto'),
}))
});
async function monitorCompetitorPrices(competitorUrl: string) {
// Las claves de Browserbase y del modelo se leen de .env / variables de entorno
const stagehand = new Stagehand({
env: 'BROWSERBASE',
model: 'anthropic/claude-sonnet-5-5', // modelos actuales: página "Lo vigente"
verbose: 1,
});
await stagehand.init();
const page = stagehand.context.pages()[0];
try {
// Vamos a la página del catálogo
await page.goto(competitorUrl);
await page.waitForLoadState('networkidle');
// Si hay que aceptar cookies, lo hacemos
const cookieBanner = await stagehand.observe(
'¿Hay un aviso de cookies o de GDPR que haya que cerrar?'
);
if (cookieBanner.length > 0) {
await stagehand.act('Haz clic en "Aceptar todo" o cierra el aviso de cookies');
}
// Extraemos los productos de la página actual
const result = await stagehand.extract(
'Encuentra todos los productos del catálogo: nombre, precio, precio anterior y disponibilidad',
PriceSchema
);
// Revisamos si hay paginación
const hasNextPage = await stagehand.observe(
'¿Hay un botón de "Página siguiente" o "Más productos"?'
);
if (hasNextPage.length > 0) {
await stagehand.act('Haz clic en la página siguiente');
await page.waitForLoadState('networkidle');
const nextPageResult = await stagehand.extract(
'Encuentra todos los productos de esta página',
PriceSchema
);
result.products.push(...nextPageResult.products);
}
return result.products;
} finally {
await stagehand.close();
}
}
// Arranque
const prices = await monitorCompetitorPrices('https://competitor.com/catalog/laptops');
console.log(`Se encontraron ${prices.length} productos`);Comparación con Playwright MCP
En la lección Automatización del navegador trabajamos con Playwright y Claude Code: una herramienta que permite controlar el navegador dentro de una conversación con Claude. Es excelente para automatización interactiva guiada por una persona.
| Característica | Playwright MCP | Stagehand + Browserbase |
|---|---|---|
| Control | Interactivo (desde Claude Code) | Autónomo (el agente decide) |
| Infraestructura | Navegador local | Nube (muchos en paralelo) |
| Antidetección | Básica | Ampliada (stealth + proxies, sin garantías) |
| Session replay | No | Sí |
| Escala | 1 navegador | Muchos a la vez (según el plan) |
| Mejor para | Tareas puntuales con una persona involucrada | Tareas autónomas regulares |
Para un agente en producción que corre por horario y procesa decenas de sitios, la opción es Stagehand. Para explorar un sitio de forma interactiva en Claude Code, Playwright MCP es más cómodo.
Hazlo tú mismo: Puppeteer + la API de Claude
Si no quieres pagar Browserbase, puedes armar algo equivalente por tu cuenta. El plugin stealth de puppeteer-extra se mantiene de forma irregular: antes de usarlo, revisa que siga funcionando:
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import Anthropic from '@anthropic-ai/sdk';
puppeteer.use(StealthPlugin());
const client = new Anthropic();
async function aiScrapePage(url: string, task: string) {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
// Imitar un navegador real
await page.setUserAgent(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
);
await page.goto(url, { waitUntil: 'networkidle2' });
// Tomamos una captura y se la pasamos a Claude
const screenshot = await page.screenshot({ encoding: 'base64' });
const htmlContent = await page.content();
const response = await client.messages.create({
model: 'claude-sonnet-5-5', // modelos actuales: página "Lo vigente"
max_tokens: 2000,
messages: [{
role: 'user',
content: [
{
type: 'image',
source: { type: 'base64', media_type: 'image/png', data: screenshot as string }
},
{
type: 'text',
text: `Tarea: ${task}\n\nHTML de la página:\n${htmlContent.substring(0, 5000)}\n\nDevuelve el resultado en JSON.`
}
]
}]
});
await browser.close();
return response.content.map((b) => (b.type === 'text' ? b.text : '')).join('');
}Este enfoque no cuesta nada en infraestructura (solo la API de Claude), pero no escala tan bien y requiere más configuración.
Antidetección: cómo no terminar bloqueado
Los sitios usan varios niveles de protección contra bots:
Nivel 1, básico (se resuelve fácil):
- Revisión del User-Agent: se resuelve poniendo un UA real
- Solicitudes demasiado rápidas: se resuelve con pausas aleatorias (
Math.random() * 2000 + 500ms) - Falta de cookies/localStorage: se resuelve con un navegador normal
Nivel 2, avanzado (requiere esfuerzo):
- Fingerprinting del navegador (canvas, WebGL, fuentes): Browserbase y puppeteer-stealth lo ocultan en parte
- Análisis del movimiento del mouse: hacen falta movimientos realistas
- Reputación de la IP: proxies residenciales (residential proxies) de proveedores como Bright Data u Oxylabs
Nivel 3, protección seria:
- CAPTCHA (reCAPTCHA v3, hCaptcha): existen servicios de pago para resolverlos, como 2captcha.com o CapSolver (el precio, en sus sitios)
- Cloudflare Bot Management: ningún servicio garantiza saltárselo
Importante. Saltarse la protección contra bots muchas veces viola los términos de uso del sitio, y en algunos países también la ley. La lección muestra la mecánica para que entiendas cómo funciona. Si el sitio tiene una API oficial o puedes acordar un acceso, eso es más confiable y más seguro.
// Ejemplo esquemático de integración con 2captcha (revisa la API del servicio y las condiciones del sitio)
import Captcha2 from '2captcha-ts';
const solver = new Captcha2.Solver(process.env.CAPTCHA_API_KEY);
// Al detectar un CAPTCHA
const siteKey = await page.evaluate(() =>
document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')
);
if (siteKey) {
const result = await solver.recaptcha({
pageurl: page.url(),
googlekey: siteKey,
});
await page.evaluate((token) => {
(window as any).grecaptcha?.enterprise?.execute?.(token);
}, result.data);
}Ética y legalidad: qué se vale y qué no
Lo que normalmente se vale (datos públicos):
- ✅ Precios en las páginas públicas de las tiendas
- ✅ Anuncios de acceso público (inmuebles, empleo)
- ✅ Artículos de noticias (para agregarlos, no para republicarlos)
- ✅ Datos de APIs públicas
- ✅ Información de empresas de fuentes abiertas
Lo que no se vale (riesgos legales):
- ❌ PII: datos personales de usuarios (emails, teléfonos de secciones cerradas)
- ❌ Contenido detrás de un paywall sin suscripción
- ❌ Datos cuyo scraping prohíben de forma explícita los Terms of Service
- ❌ Una carga que afecte el funcionamiento del sitio (comportamiento tipo DDoS)
- ❌ Saltarse los sistemas de autenticación
Antes de arrancar, revisa siempre el robots.txt y los Terms of Service del sitio. En la UE, revisa además el GDPR; en EE. UU., la CFAA y las resoluciones sobre scraping del caso LinkedIn vs. hiQ. En tu país, revisa la ley local de protección de datos personales.
Regla práctica: si la información se muestra a cualquier usuario sin iniciar sesión ni registrarse, lo más probable es que el scraping de esos datos públicos sea aceptable. Si hace falta iniciar sesión, ya es zona gris.
Monetización: vender flujos de datos
Los agentes de navegador no son solo una herramienta para uso propio. Son un producto que se puede vender.
Modelos de venta:
Flujo de datos recurrente: el cliente paga por una entrega periódica. Por ejemplo, un volcado diario de precios de la competencia en un nicho. Configuras el agente una vez, luego corre por horario, y tú vigilas los errores y ajustas el guion cuando el sitio cambia.
Proyecto puntual de recolección de datos: el cliente paga por una sola entrega. Una base de empresas de una región, una lista de posibles socios, un análisis de mercado.
Monitoring as a service: seguimiento de cambios en sitios (vacantes nuevas, cambios de precio, competidores nuevos). El cliente recibe alertas por mensaje o por email.
Solución white-label: no vendes datos, vendes la herramienta. Configuras el agente para el cliente y él lo usa. Pago único + soporte.
El precio lo ponen el mercado y tus costos (servidores, el plan de Browserbase, tokens, tiempo de soporte). Los ingresos en este nicho no están garantizados: calcula gastos y demanda en tu mercado con las lecciones Empaquetar la oferta y Precios y monetización.
Práctica
Tarea: un agente que monitorea precios con avisos en Telegram
Vamos a construir un agente que cada hora revisa los precios de los productos de la competencia y manda una alerta si un precio bajó más de 5%. Usamos Telegram porque crear un bot ahí es gratis y rápido; puedes cambiarlo por el canal que prefieras.
Paso 1: instalar dependencias
mkdir price-monitor-agent && cd price-monitor-agent
npm init -y
npm install @browserbasehq/stagehand zod dotenv node-telegram-bot-api
npm install -D typescript @types/node tsxCrea .env:
BROWSERBASE_API_KEY=bb_live_xxxx
BROWSERBASE_PROJECT_ID=prj_xxxx
TELEGRAM_BOT_TOKEN=xxxx
TELEGRAM_CHAT_ID=xxxx
ANTHROPIC_API_KEY=sk-ant-xxxxPaso 2: esquema de datos y configuración
Crea src/types.ts:
import { z } from 'zod';
export const ProductSchema = z.object({
name: z.string(),
price: z.number(),
oldPrice: z.number().nullable(),
inStock: z.boolean(),
sku: z.string().optional(),
});
export type Product = z.infer<typeof ProductSchema>;
export const MonitorConfig = {
targetUrl: 'https://example-shop.com/catalog/laptops',
checkIntervalMinutes: 60,
priceDropThresholdPercent: 5,
};Paso 3: la función de scraping
Crea src/scraper.ts:
import { Stagehand } from '@browserbasehq/stagehand';
import { z } from 'zod';
import { ProductSchema, type Product } from './types';
export async function scrapeProducts(url: string): Promise<Product[]> {
// Las claves de Browserbase y del modelo se leen de .env (dotenv carga el archivo .env)
const stagehand = new Stagehand({
env: 'BROWSERBASE',
model: 'anthropic/claude-sonnet-5-5',
});
await stagehand.init();
const page = stagehand.context.pages()[0];
try {
await page.goto(url);
await page.waitForLoadState('networkidle');
// Manejo de avisos de cookies
const cookieBanner = await stagehand.observe(
'¿Hay un aviso de cookies o de consentimiento que haya que aceptar?'
);
if (cookieBanner.length > 0) {
await stagehand.act('Acepta o cierra el aviso de cookies');
await page.waitForTimeout(1000);
}
// Extraemos los datos de los productos
const result = await stagehand.extract(
`
Encuentra todos los productos de la página del catálogo.
Para cada uno extrae: el nombre exacto, el precio actual como número,
el precio anterior (si hay descuento) y si hay existencias.
Los precios deben ser números sin símbolos de moneda.
`,
z.object({
products: z.array(ProductSchema)
})
);
return result.products;
} finally {
await stagehand.close();
}
}Paso 4: guardar y comparar precios
Crea src/storage.ts:
import fs from 'fs/promises';
import path from 'path';
import type { Product } from './types';
const DATA_FILE = path.join(process.cwd(), 'prices.json');
export interface PriceRecord {
timestamp: string;
products: Product[];
}
export async function saveProducts(products: Product[]): Promise<void> {
const record: PriceRecord = {
timestamp: new Date().toISOString(),
products,
};
await fs.writeFile(DATA_FILE, JSON.stringify(record, null, 2));
}
export async function loadPreviousProducts(): Promise<Product[] | null> {
try {
const data = await fs.readFile(DATA_FILE, 'utf-8');
const record: PriceRecord = JSON.parse(data);
return record.products;
} catch {
return null; // Primera ejecución
}
}
export interface PriceDrop {
product: Product;
oldPrice: number;
newPrice: number;
dropPercent: number;
}
export function findPriceDrops(
current: Product[],
previous: Product[],
thresholdPercent: number
): PriceDrop[] {
const drops: PriceDrop[] = [];
for (const currentProduct of current) {
const prevProduct = previous.find(p => p.name === currentProduct.name);
if (!prevProduct) continue;
const dropPercent = ((prevProduct.price - currentProduct.price) / prevProduct.price) * 100;
if (dropPercent >= thresholdPercent) {
drops.push({
product: currentProduct,
oldPrice: prevProduct.price,
newPrice: currentProduct.price,
dropPercent,
});
}
}
return drops.sort((a, b) => b.dropPercent - a.dropPercent);
}Paso 5: avisos y ciclo principal
Crea src/index.ts:
import 'dotenv/config';
import TelegramBot from 'node-telegram-bot-api';
import { scrapeProducts } from './scraper';
import { saveProducts, loadPreviousProducts, findPriceDrops } from './storage';
import { MonitorConfig } from './types';
const bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN!);
async function sendAlert(drops: ReturnType<typeof findPriceDrops>) {
const chatId = process.env.TELEGRAM_CHAT_ID!;
let message = `🔥 *¡Se detectaron bajas de precio!*\n\n`;
for (const drop of drops) {
message += `📦 *${drop.product.name}*\n`;
message += `💰 $${drop.oldPrice.toLocaleString()} → $${drop.newPrice.toLocaleString()}\n`;
message += `📉 Baja: *${drop.dropPercent.toFixed(1)}%*\n`;
message += drop.product.inStock ? '✅ Hay existencias\n' : '❌ Sin existencias\n';
message += '\n';
}
await bot.sendMessage(chatId, message, { parse_mode: 'Markdown' });
}
async function runCheck() {
console.log(`[${new Date().toISOString()}] Iniciando revisión de precios...`);
try {
// Scraping de los precios actuales
const currentProducts = await scrapeProducts(MonitorConfig.targetUrl);
console.log(`Productos encontrados: ${currentProducts.length}`);
// Comparación con los datos anteriores
const previousProducts = await loadPreviousProducts();
if (previousProducts) {
const drops = findPriceDrops(
currentProducts,
previousProducts,
MonitorConfig.priceDropThresholdPercent
);
if (drops.length > 0) {
console.log(`Bajas de precio encontradas: ${drops.length}`);
await sendAlert(drops);
} else {
console.log('No se detectaron cambios de precio importantes');
}
} else {
console.log('Primera ejecución: precios base guardados');
}
// Guardamos los precios actuales como referencia
await saveProducts(currentProducts);
} catch (error) {
console.error('Error en la revisión:', error);
await bot.sendMessage(
process.env.TELEGRAM_CHAT_ID!,
`⚠️ Error del agente de monitoreo: ${error}`
);
}
}
// Arranque y repetición por horario
async function main() {
console.log('Agente de monitoreo iniciado');
// Primera ejecución de inmediato
await runCheck();
// Después, por horario
setInterval(
runCheck,
MonitorConfig.checkIntervalMinutes * 60 * 1000
);
}
main().catch(console.error);Arranque:
npx tsx src/index.tsPara producción: un VPS sencillo con pm2 start, o correr una sola pasada por horario con cron en lugar de setInterval.
Herramientas y recursos
| Herramienta | Para qué | Enlace |
|---|---|---|
| Browserbase | Navegadores administrados en la nube para agentes | browserbase.com |
| Stagehand | SDK de Playwright con IA (código abierto) | github.com/browserbase/stagehand |
| Playwright | El motor base de automatización del navegador | playwright.dev |
| Puppeteer + Stealth | Alternativa hazlo-tú-mismo con plugin antidetección | github.com/berstend/puppeteer-extra |
| 2captcha | Servicio para resolver CAPTCHA (precio en su sitio) | 2captcha.com |
| CapSolver | Alternativa a 2captcha con IA | capsolver.com |
| Bright Data | Proxies residenciales para evitar bloqueos | brightdata.com |
Ideas clave
"Un agente de navegador no es un script mejorado. Es un empleado que entiende el objetivo, no solo sigue instrucciones. Cuando el sitio cambia, una persona se adapta. El agente también."
"Un agente de monitoreo ya configurado se puede ofrecer como servicio, pero no es un ingreso pasivo: los sitios cambian y el agente necesita mantenimiento. Cuánto deje depende del nicho y de los clientes; no hay garantías. Browserbase y Stagehand permiten armar rápido un prototipo que funcione."
"Los datos públicos son como un aparador. Mirar y analizar es tu derecho. Pero romper la cerradura o copiar contenido ajeno con fines comerciales ya es otra historia. Conoce el límite."
Siguiente lección
→ Orquestación multiagente: LangGraph, CrewAI y Mastra
Si un agente es un empleado, un sistema multiagente es todo un departamento. Vemos cómo se coordinan los agentes, cómo se pasan tareas entre sí y cómo construir un pipeline con decenas de trabajadores de IA especializados. LangGraph para workflows con estado, CrewAI para equipos por roles, Mastra como framework de TypeScript de código abierto.
La marca se guarda solo en este navegador y no se envía a ningún sitio. Mi progreso