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


basketeer filtering and ranking a live Tesco search by nutrition, then reading a product's micronutrients

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.



basketeer searching Tesco from the CLI, live results, no browser

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.


CI tests license: MIT node >=18

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 fetch puro 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/failed informan 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álogosearch, getProduct, getProducts por lotes, browseCategory (anónimo); favourites / "mis habituales" (autenticado)
  • Imágenes de productosimageUrl en cada producto/resultado; resizeImageUrl(url, { width, height }) para miniaturas (anónimo)
  • Cestaadd, set, remove, get
  • Franjas horarias — entrega y recogida: list / book / release
  • Pedidoslist, amend, cancel, lastFulfilled (reordenar)
  • Pagocheckout() 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 fetch puro. Sin estado, sin navegador, limitado a un cortés 1 req/s, con una parada dura en 429/403 (sin tormentas de reintentos). Las lecturas anónimas (búsqueda, producto, navegación, nutrición) solo necesitan la x-apikey pú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. BrowserAuthBackend maneja tu Google Chrome instalado para iniciar sesión una vez y recoge la sesión (un portador OAuth.AccessToken má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 terminas el pago; llenas la cesta y reservas una franja horaria con las llamadas anteriores, y checkout() 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

The basketeer CLI command palette

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). La x-apikey incluida rota aproximadamente mensualmente. Establece la tuya con la variable de entorno TESCO_API_KEY o new Basketeer({ apiKey }). No reintentable.
  • AuthExpiredError (la sesión no se pudo renovar). Ejecuta basketeer login de 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.
  • AuthExpiredError en un 401. Un solo 401 dispara una renovación transparente del navegador y reintento; un 401 persistente aparece como AuthExpiredError.
  • "Canal de Chrome no encontrado" al iniciar sesión. BrowserAuthBackend maneja el Google Chrome del sistema (canal chrome). Instala Chrome, o ejecuta npx playwright install chrome. Asegúrate de que el playwright opcional 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 locationUuid para 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