Basketeer
Gestiona una cuenta personal de compras Tesco en Reino Unido: búsqueda, cesta, franjas de entrega, pedidos y nutrición en envase. Filtra y clasifica productos por macros y micros. Las herramientas de catálogo y nutrición no requieren autenticación.
Documentación
basketeer
Un SDK de TypeScript tipado y puramente HTTP para tu propia cuenta de Tesco, con la información nutricional del envase normalizada en datos tipados.
Haz tu compra semanal desde código, la terminal o un agente de IA. Todo excepto iniciar sesión y pagar es fetch puro, y los productos vuelven con su información nutricional del envase normalizada en macros tipados y micronutrientes que puedes buscar y clasificar.
No oficial · no afiliado a Tesco · para automatizar tu propia cuenta · MIT
Filtra y clasifica una búsqueda en vivo por información nutricional del envase (proteína ≥ 10g, azúcar ≤ 7g), luego lee los macros y micronutrientes completos de cualquier producto. Datos reales, sin inicio de sesión.
La búsqueda simple en el catálogo es una línea: basketeer search "oat milk" canalizado a jq. Real, en vivo, sin inicio de sesión.
Por qué basketeer
Tesco no tiene una API pública, y el enfoque habitual (raspar el DOM) se rompe con el próximo rediseño del sitio, se detiene en títulos y precios, y no puede ser manejado por un agente de IA. basketeer habla directamente con la puerta de enlace GraphQL de Tesco:
- Consciente de la nutrición. Cuando Tesco lista la información nutricional del envase, basketeer la normaliza en macros tipados y micronutrientes estructurados, gratis en lecturas anónimas, y te permite filtrar y clasificar una búsqueda por ellos (
searchByNutrition). Una API de primera clase, no un complemento raspado. - Robusto. GraphQL puro por HTTP, no raspado del DOM. Un rediseño cosmético del sitio no lo romperá.
- Completo. Reserva, modifica, cancela y reordena una compra a domicilio. El ciclo de vida completo del pedido, no solo "añadir a la cesta".
- Listo para agentes. Un servidor MCP stdio permite que Claude o cualquier cliente MCP haga la compra. Las herramientas de solo lectura y destructivas están anotadas, y el pago nunca se realiza.
- Tipado y ligero. Un cliente completamente tipado que importas; el CLI y el servidor MCP están construidos sobre él. La ruta de datos no importa paquetes de terceros (las tres dependencias de ejecución — commander, el SDK de MCP y zod — solo las usan el CLI y el servidor MCP). Es
fetchpuro sin APIs exclusivas de Node, por lo que se ejecuta en Node y en runtimes compatibles con Node. - Seguro.
checkout()se detiene en la URL de pago. Un humano completa la autenticación 3-D Secure en un navegador, por diseño. - Probado. 75 pruebas en el plano de datos y sus analizadores.
Nutrición, la parte que nada más tiene
Cuando Tesco lista la información nutricional del envase de un producto, basketeer la normaliza en macros tipados (energía, proteína, grasa, saturados, carbohidratos, azúcares, fibra, sal) y micronutrientes estructurados (una entrada nombrada por cada vitamina y mineral, con cantidad, unidad y % del Valor de Referencia de Nutrientes). Gratis, en lecturas anónimas (nutrition es null cuando un producto no tiene filas utilizables). Y puedes buscar y clasificar por ello:
# "high-protein yogurt, >=10g protein, <=7g sugar, ranked by protein" — live, no login
basketeer search "high protein yogurt" --min-protein 10 --max-sugar 7 --sort protein
import { Basketeer } from "basketeer";
const client = new Basketeer(); // no auth needed for nutrition reads
const { results, hydrated, failed } = await client.searchByNutrition("high protein yogurt", {
where: { protein: { min: 10 }, sugars: { max: 7 } },
sort: { by: "protein", dir: "desc" },
});
results[0]?.macros; // { energyKcal, protein, fat, saturates, carbs, sugars, fibre, salt }
results[0]?.nutrition?.micros; // [{ name: "Calcium", amount: 120, unit: "mg", nrvPercent: 15 }, ...]
La búsqueda filtrada por nutrición ejecuta una búsqueda por palabras clave, luego obtiene la nutrición de cada candidato (una llamada de producto limitada por cada uno, limitada por
hydrate, por defecto 20) y filtra localmente. Filtra dentro de una búsqueda; no escanea todo el catálogo.hydrated/failedinforman el costo exacto.
[!IMPORTANTE] No afiliado, respaldado o conectado a Tesco. Este es un cliente no oficial e ingeniado inversamente para automatizar tu propia cuenta, en el espíritu de la interoperabilidad personal. Puede romperse si Tesco cambia su API. Úsalo para tus propias compras, bajo tu propio riesgo, dentro de los términos de Tesco. No para reventa, raspado a gran escala u operar cuentas que no sean tuyas. Consulta Ética y uso.
Inicio rápido
npm install basketeer
Lecturas anónimas, cero configuración
La búsqueda en el catálogo, la consulta de productos y la nutrición solo necesitan la clave de API pública:
import { Basketeer } from "basketeer";
const client = new Basketeer();
const { results } = await client.search("wholemeal bread", { limit: 10 });
const top = results[0];
if (top) {
const product = await client.getProduct(top.sku);
console.log(product.title, product.price.actual);
// => "Tesco Wholemeal Bread 800G" 0.75
}
// Fetch up to 15 SKUs in one throttled HTTP request. Duplicates are fetched
// once and missing products are omitted.
const products = await client.getProducts(["282822189", "275280804"]);
Autenticado, inicia sesión una vez, luego HTTP puro
El inicio de sesión está detrás de las defensas antirrobots de Akamai, por lo que un navegador real crea la sesión una vez. Después, el plano de datos es fetch puro; solo una renovación de token (aproximadamente una vez por hora) reabre brevemente el navegador.
npm install basketeer playwright # playwright is an optional peer dep, only used for sign-in
npx playwright install chrome # the Chrome channel sign-in drives (skip if you already have Google Chrome)
import { Basketeer, FileTokenStore } from "basketeer";
import { BrowserAuthBackend } from "basketeer/auth/browser/playwright";
const store = new FileTokenStore(); // ~/.basketeer/session.json
const authBackend = new BrowserAuthBackend(); // drives your installed Google Chrome
// First run: a Chrome window opens, you sign in once, the session is harvested.
await new Basketeer({ store, authBackend }).login();
// Any later process: resume. Data calls are pure fetch; refresh reopens the browser.
const client = await Basketeer.resume({ store, authBackend });
// 1. Find things, then build the basket.
const milk = (await client.search("semi skimmed milk", { limit: 5 })).results[0];
if (milk) await client.basket.add(milk.sku, 2); // add 2 (increments the line)
// Your "usuals" (needs auth). Set exact quantities for the first few:
const usuals = (await client.favourites({ limit: 50 })).results;
for (const item of usuals.slice(0, 3)) await client.basket.set(item.sku, 1); // 0 removes
// 2. Book a delivery slot.
const slots = await client.slots.list(); // today..+6 days
const free = slots.find((s) => s.status === "Available");
if (free) await client.slots.book(free.id); // held until reservationExpiry
// 3. Hand off to the browser for payment. The SDK stops here, on purpose.
const { url } = await client.checkout();
console.log("Finish payment in a browser:", url);
Capacidades
El ciclo de vida completo de la compra, tipado de principio a fin:
- Nutrición — macros tipados y micros estructurados, normalizados de las filas del envase de un producto cuando están presentes; filtra y clasifica una búsqueda por nutrición (anónimo)
- Catálogo —
search,getProduct,getProductspor lotes,browseCategory(anónimo);favourites/ "mis habituales" (autenticado) - Imágenes de productos —
imageUrlen cada producto/resultado;resizeImageUrl(url, { width, height })para miniaturas (anónimo) - Cesta —
add,set,remove,get - Franjas horarias — entrega y recogida:
list/book/release - Pedidos —
list,amend,cancel,lastFulfilled(reordenar) - Pago —
checkout()devuelve la URL de pago; nunca paga
→ Referencia completa (firmas, tipos de retorno, el catálogo de errores y dónde se ejecuta el navegador): docs/api.md
Cómo funciona
El sitio web de Tesco habla con una puerta de enlace GraphQL en xapi.tesco.com. basketeer habla ese protocolo directamente.
- El plano de datos es HTTP puro. Búsqueda, producto, cesta, franjas horarias y pedidos son operaciones GraphQL sobre
fetchpuro. Sin estado, sin navegador, limitado a un cortés 1 req/s, con una parada dura en429/403(sin tormentas de reintentos). Las lecturas anónimas (búsqueda, producto, navegación, nutrición) solo necesitan lax-apikeypública;favourites, cesta, franjas horarias y pedidos necesitan una sesión. - Se necesita un navegador solo para la autenticación. El inicio de sesión está protegido por Akamai (huella TLS más un desafío JS) que solo un navegador genuino satisface.
BrowserAuthBackendmaneja tu Google Chrome instalado para iniciar sesión una vez y recoge la sesión (un portadorOAuth.AccessTokenmás cookies). El token de acceso dura aproximadamente una hora y se renueva a través de la misma ruta del navegador; la sesión subyacente dura aproximadamente 30 días. - El pago está deliberadamente fuera de alcance. Pagar pasa por una aplicación de pago separada protegida por CSRF y autenticación de tarjeta 3-D Secure. Eso está ligado al navegador y es sensible al fraude por naturaleza.
checkout()devuelve la cesta actual y la URL donde tú terminas el pago; llenas la cesta y reservas una franja horaria con las llamadas anteriores, ycheckout()solo hace la entrega. basketeer nunca paga.
Autenticación, tú eliges dónde se ejecuta el navegador
La biblioteca no depende de ningún navegador. Solo necesita un Session. Ejecuta el navegador en la máquina del usuario (BrowserAuthBackend más el playwright opcional), bajo Xvfb en un contenedor de larga duración, en un navegador alojado residencial para serverless, o omítelo por completo y entrega cookies que recogiste tú mismo:
import { Basketeer, sessionFromCookies } from "basketeer";
// Got cookies from your own browser anywhere? Hand them straight in:
const session = sessionFromCookies(myCookieList); // {name,value}[] => Session
const client = new Basketeer({ session }); // reads + writes, pure HTTP
Implementa tu propio backend con el AuthBackend de dos métodos (login, refresh) y el TokenStore de tres métodos (load, save, clear). FileTokenStore y MemoryTokenStore se incluyen de serie. La matriz completa de hosts está en docs/api.md.
Nota sobre serverless. Una función serverless no puede mantener un navegador, y Akamai de Tesco bloquea el inicio de sesión desde IPs de centro de datos, por lo que un navegador alojado necesita una salida residencial. Los proxies de navegadores gestionados listos para usar (Browserbase y similares) también suelen estar bloqueados para dominios de supermercados. El patrón fiable es un navegador en una conexión residencial que controles (un servidor doméstico, una Raspberry Pi, el dispositivo del usuario), con el plano de datos HTTP puro ejecutándose en cualquier lugar.
Pedidos y modificaciones
const orders = await client.orders.list();
for (const o of orders) console.log(o.orderNo, o.status, o.totalPrice, "amend until", o.amendExpiry);
// Amend returns a scoped handle; basket edits apply to THAT order.
const amendment = await client.orders.amend(orders[0]!.orderNo);
await amendment.remove("258114107");
await amendment.set("292632440", 1);
// ...then check out again to commit (pays any difference), or:
await amendment.discard(); // leave the order unchanged
client.amendingOrderNo; // the order currently open for amendment, or null
await client.orders.cancel(orders[0]!.orderNo);
// "Reorder my usual shop":
const last = await client.orders.lastFulfilled();
for (const it of last?.items ?? []) await client.basket.set(it.productId!, it.quantity, it.unit ?? "pcs");
// Completed orders, newest first, offset-paged (Tesco has no cursor or total).
// The result set is LIVE — dedupe by order.id and never persist nextOffset.
let page = await client.orders.history(); // { orders, nextOffset }
while (page.nextOffset !== null) page = await client.orders.history({ offset: page.nextOffset });
Servidor MCP (para agentes de IA)
Un servidor MCP stdio se incluye como el binario basketeer-mcp, exponiendo herramientas (basketeer_search, basketeer_search_by_nutrition, basketeer_nutrition, basketeer_basket_set, basketeer_slots_list, basketeer_orders_list, basketeer_checkout, …) para que Claude Desktop o cualquier cliente MCP pueda hacer la compra. Las herramientas de solo lectura llevan readOnlyHint; las mutables llevan destructiveHint, y basketeer_orders_cancel / basketeer_checkout requieren un token de confirmación en dos pasos. basketeer_checkout devuelve la URL de pago para el humano. No hay herramienta de "pagar". Las herramientas de búsqueda aceptan un select opcional — un array de rutas en notación de puntos (p. ej. ["sku", "title", "price.actual", "promotions.description"]) que recorta cada resultado a solo esos campos, manteniendo bajo el uso de tokens en bucles de agentes.
// claude_desktop_config.json — run `basketeer login` once first so it has a session.
{
"mcpServers": {
"basketeer": { "command": "npx", "args": ["-y", "-p", "basketeer", "basketeer-mcp"] }
}
}
CLI

El binario basketeer imprime JSON en stdout, errores codificados en stderr. Instala globalmente para el comando simple, o prefija con npx -p basketeer:
basketeer login # one-time browser sign-in
basketeer search "oat milk" --limit 5
basketeer search "high protein yogurt" --min-protein 10 --max-sugar 7 --sort protein
basketeer product 254656543
basketeer nutrition 292990463 # normalized macros + micros for a product
basketeer favourites
basketeer basket add 258114107 1 # increment; basket set <sku> <qty> for exact
basketeer slots # --collection for click-and-collect
basketeer orders list
basketeer checkout # prints the payment URL; you finish in a browser
Ejemplos
Scripts ejecutables en examples/: lookup.ts (anónimo), login.ts, shop-flow.ts (búsqueda → cesta → franja horaria → entrega de pago), orders.ts y bring-your-own-auth.ts.
Solución de problemas
Todo lo lanzado es una subclase de BasketeerError, por lo que puedes ramificar según el tipo. Los casos comunes:
ApiKeyError(la clave pública fue rechazada). Lax-apikeyincluida rota aproximadamente mensualmente. Establece la tuya con la variable de entornoTESCO_API_KEYonew Basketeer({ apiKey }). No reintentable.AuthExpiredError(la sesión no se pudo renovar). Ejecutabasketeer loginde nuevo. Los hosts sin cabeza no pueden renovar (Akamai bloquea el inicio de sesión sin cabeza), por lo que alcanzan el límite de token de ~1h y deben volver a iniciar sesión en una máquina con pantalla.RateLimitedError(429/403). El cliente se detiene en lugar de crear tormentas de reintentos. Retrocede; ya limita a 1 req/s por defecto.AuthExpiredErroren un401. Un solo401dispara una renovación transparente del navegador y reintento; un401persistente aparece comoAuthExpiredError.- "Canal de Chrome no encontrado" al iniciar sesión.
BrowserAuthBackendmaneja el Google Chrome del sistema (canalchrome). Instala Chrome, o ejecutanpx playwright install chrome. Asegúrate de que elplaywrightopcional esté instalado.
Seguridad y almacenamiento de sesión
FileTokenStore escribe ~/.basketeer/session.json que contiene un token portador y cookies en texto plano. Trátalo como una contraseña: mantén sus permisos de archivo estrictos, nunca lo confirmes y límpialo (store.clear()) en una máquina compartida. Para contextos efímeros o de servidor usa MemoryTokenStore o tu propio TokenStore, y mantén la sesión fuera de los registros.
Limitaciones conocidas
- Solo Tesco Reino Unido. Construido contra la puerta de enlace de comestibles del Reino Unido; otras regiones no están probadas.
- Pre-lanzamiento (v0.1). La API pública puede cambiar entre versiones menores hasta 1.0.
- Ingeniería inversa. Sin contrato público de Tesco; una operación o la clave pública pueden cambiar y romper una llamada hasta que se actualice.
- La autenticación necesita un navegador real en una conexión residencial. Las IPs de centro de datos están bloqueadas para el inicio de sesión; el plano de datos HTTP puro se ejecuta en cualquier lugar.
- La búsqueda nutricional está limitada, no es de todo el catálogo: filtra dentro de una búsqueda por palabras clave, limitada por
hydrate. - Las franjas horarias de recogida necesitan un
locationUuidpara la tienda donde recoges.
Ética y uso
Automatización de interoperabilidad de cuentas personales: tu cuenta, tus datos. El cliente usa por defecto 1 solicitud/segundo, concurrencia única, y se detiene en 429/403. Por favor, mantenlo así. No para reventa, raspado masivo u operación de múltiples cuentas. Este proyecto no está afiliado a Tesco; "Tesco" es una marca comercial de su propietario y se usa aquí solo para describir interoperabilidad.
Desarrollo
npm install
npm test # 75 tests: vitest unit + regression + smoke
npm run build # clean build to dist/
npm run example:lookup
Se aceptan PRs. Mantén el código legible y mínimo, añade una prueba para cualquier cambio de comportamiento y nunca confirmes una sesión o clave de API.
Licencia
MIT © Toby Andrews