Basketeer
Gerencie uma conta pessoal de supermercado Tesco no Reino Unido: pesquisa, cesta, horários de entrega, pedidos e informações nutricionais na embalagem. Filtre e classifique produtos por macronutrientes e micronutrientes. As ferramentas de catálogo e nutrição não exigem autenticação.
Documentação
basketeer
Um SDK TypeScript tipado e 100% HTTP para sua própria conta de supermercado Tesco, com a nutrição da embalagem normalizada em dados tipados.
Faça suas compras semanais a partir do código, do terminal ou de um agente de IA. Tudo, exceto login e pagamento, é fetch puro, e os produtos retornam com a nutrição da embalagem normalizada em macros e micronutrientes tipados, que você pode pesquisar e classificar.
Não oficial · sem afiliação com a Tesco · para automatizar sua própria conta · MIT
Filtre e classifique uma busca ao vivo por nutrição da embalagem (proteína ≥ 10g, açúcar ≤ 7g) e leia os macros e micronutrientes completos de qualquer produto. Dados reais, sem login.
A busca simples no catálogo é uma linha: basketeer search "oat milk" canalizado para jq. Real, ao vivo, sem login.
Por que basketeer
A Tesco não tem API pública, e a abordagem usual (raspar o DOM) quebra no próximo redesenho do site, para em títulos e preços, e não pode ser dirigida por um agente de IA. O basketeer fala diretamente com o gateway GraphQL da Tesco:
- Consciente da nutrição. Onde a Tesco lista a nutrição da embalagem, o basketeer a normaliza em macros tipados e micronutrientes estruturados, gratuitos em leituras anônimas, e permite filtrar e classificar uma busca por eles (
searchByNutrition). Uma API de primeira classe, não um complemento raspado. - Robusto. GraphQL 100% HTTP, não raspagem de DOM. Um redesenho cosmético do site não vai quebrá-lo.
- Completo. Agende, altere, cancele e reordene uma entrega. O ciclo de vida completo do pedido, não apenas "adicionar ao carrinho".
- Pronto para agentes. Um servidor MCP stdio permite que Claude ou qualquer cliente MCP faça as compras. Ferramentas somente leitura e destrutivas são anotadas, e o checkout nunca paga.
- Tipado e enxuto. Um cliente totalmente tipado que você importa; o CLI e o servidor MCP são construídos sobre ele. O caminho de dados não importa pacotes de terceiros (as três dependências de runtime — commander, o SDK MCP e zod — são puxadas apenas pelo CLI e pelo servidor MCP). É
fetchpuro, sem APIs exclusivas do Node, então roda em Node e runtimes compatíveis com Node. - Seguro.
checkout()para na URL de pagamento. Um humano finaliza o 3-D Secure no navegador, por design. - Testado. 75 testes no plano de dados e seus parsers.
Nutrição, a parte que ninguém mais tem
Quando a Tesco lista a nutrição da embalagem de um produto, o basketeer a normaliza em macros tipados (energia, proteína, gordura, saturados, carboidratos, açúcares, fibra, sal) e micronutrientes estruturados (uma entrada nomeada por vitamina e mineral, com quantidade, unidade e % do Valor de Referência de Nutriente). Grátis, em leituras anônimas (nutrition é null quando um produto não tem linhas utilizáveis). E você pode pesquisar e classificar por isso:
# "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 }, ...]
A busca filtrada por nutrição executa uma busca por palavra-chave e depois busca a nutrição de cada candidato (uma chamada de produto limitada por vez, limitada por
hydrate, padrão 20) e filtra localmente. Ela filtra dentro de uma busca; não varre o catálogo inteiro.hydrated/failedrelatam o custo exato.
[!IMPORTANT] Sem afiliação, endosso ou conexão com a Tesco. Este é um cliente não oficial, de engenharia reversa, para automatizar sua própria conta, no espírito de interoperabilidade pessoal. Pode quebrar se a Tesco mudar a API. Use para suas próprias compras, por sua conta e risco, dentro dos termos da Tesco. Não para revenda, raspagem em escala ou operação de contas que não sejam suas. Veja Ética e uso.
Início rápido
npm install basketeer
Leituras anônimas, zero configuração
Busca no catálogo, consulta de produto e nutrição precisam apenas da chave 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, faça login uma vez, depois HTTP puro
O login está atrás das defesas anti-bot da Akamai, então um navegador real gera a sessão uma vez. Depois disso, o plano de dados é fetch puro; apenas uma renovação de token (cerca de uma vez por hora) reabre brevemente o 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
O ciclo de vida completo do supermercado, tipado de ponta a ponta:
- Nutrição — macros tipados e micros estruturados, normalizados das linhas da embalagem do produto quando presentes; filtre e classifique uma busca por nutrição (anônimo)
- Catálogo —
search,getProduct,getProductsem lote,browseCategory(anônimo);favourites/ "meus habituais" (autenticado) - Imagens de produto —
imageUrlem cada produto/resultado;resizeImageUrl(url, { width, height })para miniaturas (anônimo) - Carrinho —
add,set,remove,get - Faixas de entrega — entrega e retirada:
list/book/release - Pedidos —
list,amend,cancel,lastFulfilled(reordenar) - Checkout —
checkout()retorna a URL de pagamento; nunca paga
→ Referência completa (assinaturas, tipos de retorno, catálogo de erros e onde o navegador roda): docs/api.md
Como funciona
O site da Tesco fala com um gateway GraphQL em xapi.tesco.com. O basketeer fala esse protocolo diretamente.
- O plano de dados é HTTP puro. Busca, produto, carrinho, faixas e pedidos são operações GraphQL sobre
fetchpuro. Sem estado, sem navegador, limitado a 1 req/s educado, com parada rígida em429/403(sem tempestades de nova tentativa). Leituras anônimas (busca, produto, navegação, nutrição) precisam apenas dox-apikeypúblico;favourites, carrinho, faixas e pedidos precisam de uma sessão. - Um navegador é necessário apenas para autenticação. O login é protegido pela Akamai (impressão digital TLS mais um desafio JS) que apenas um navegador genuíno satisfaz.
BrowserAuthBackenddirige seu Google Chrome instalado para fazer login uma vez e coleta a sessão (um bearerOAuth.AccessTokenmais cookies). O token de acesso dura cerca de uma hora e renova pelo mesmo caminho do navegador; a sessão subjacente dura cerca de 30 dias. - O pagamento está deliberadamente fora do escopo. Pagar passa por um aplicativo de checkout separado, protegido por CSRF, e autenticação de cartão 3-D Secure. Isso é vinculado ao navegador e sensível a fraudes por natureza.
checkout()retorna o carrinho atual e a URL onde você finaliza o pagamento; você preenche o carrinho e agenda uma faixa com as chamadas anteriores, e ocheckout()em si apenas faz a transferência. O basketeer nunca paga.
Autenticação, você escolhe onde o navegador roda
A biblioteca não depende de navegador. Ela só precisa de um Session. Rode o navegador na máquina do usuário (BrowserAuthBackend mais o peer opcional playwright), sob Xvfb em um contêiner de longa duração, em um navegador hospedado residencial para serverless, ou pule completamente e entregue cookies que você mesmo coletou:
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
Implemente seu próprio backend com o AuthBackend de dois métodos (login, refresh) e o TokenStore de três métodos (load, save, clear). FileTokenStore e MemoryTokenStore vêm prontos. A matriz completa de hosts está em docs/api.md.
Nota sobre serverless. Uma função serverless não pode manter um navegador, e a Akamai da Tesco bloqueia login de IPs de datacenter, então um navegador hospedado precisa de saída residencial. Proxies de navegador gerenciados prontos (Browserbase e similares) também costumam ser bloqueados para domínios de supermercado. O padrão confiável é um navegador em uma conexão residencial que você controla (um servidor doméstico, um Pi, o dispositivo do usuário), com o plano de dados HTTP puro rodando em qualquer lugar.
Pedidos e alterações
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)
Um servidor MCP stdio vem como o binário basketeer-mcp, expondo ferramentas (basketeer_search, basketeer_search_by_nutrition, basketeer_nutrition, basketeer_basket_set, basketeer_slots_list, basketeer_orders_list, basketeer_checkout, …) para que o Claude Desktop ou qualquer cliente MCP possa fazer compras. Ferramentas somente leitura carregam readOnlyHint; as de mutação carregam destructiveHint, e basketeer_orders_cancel / basketeer_checkout exigem um token de confirmação em duas etapas. basketeer_checkout retorna a URL de pagamento para o humano. Não existe ferramenta "pagar". As ferramentas de busca aceitam um select opcional — um array de caminhos em notação de ponto (ex.: ["sku", "title", "price.actual", "promotions.description"]) que reduz cada resultado a apenas esses campos, mantendo o uso de tokens baixo em loops 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

O binário basketeer imprime JSON na saída padrão e erros codificados na saída de erro. Instale globalmente para o comando direto, ou prefixe com 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
Exemplos
Scripts executáveis em examples/: lookup.ts (anônimo), login.ts, shop-flow.ts (busca → carrinho → faixa → transferência de checkout), orders.ts e bring-your-own-auth.ts.
Solução de problemas
Tudo que é lançado é uma subclasse de BasketeerError, então você pode ramificar pelo tipo. Os casos comuns:
ApiKeyError(a chave pública foi rejeitada). Ox-apikeyincluído gira aproximadamente mensalmente. Defina o seu com a variável de ambienteTESCO_API_KEYounew Basketeer({ apiKey }). Não é possível tentar novamente.AuthExpiredError(a sessão não pôde ser renovada). Rodebasketeer loginnovamente. Hosts headless não podem renovar (a Akamai bloqueia login headless), então atingem o teto de ~1h de token e precisam refazer login em uma máquina com display.RateLimitedError(429/403). O cliente para em vez de criar tempestade de novas tentativas. Recue; ele já limita a 1 req/s por padrão.AuthExpiredErrorem um401. Um único401dispara uma renovação transparente do navegador e nova tentativa; um401persistente aparece comoAuthExpiredError.- "Chrome channel not found" no login.
BrowserAuthBackenddirige o Google Chrome do sistema (canalchrome). Instale o Chrome ou rodenpx playwright install chrome. Certifique-se de que o peer opcionalplaywrightestá instalado.
Segurança e armazenamento de sessão
FileTokenStore grava ~/.basketeer/session.json contendo um token bearer e cookies em texto puro. Trate como senha: mantenha as permissões do arquivo restritas, nunca faça commit e limpe (store.clear()) em máquinas compartilhadas. Para contextos efêmeros ou de servidor, use MemoryTokenStore ou seu próprio TokenStore e mantenha a sessão fora dos logs.
Limitações conhecidas
- Apenas Tesco do Reino Unido. Construído contra o gateway de supermercados do Reino Unido; outras regiões não foram testadas.
- Pré-lançamento (v0.1). A API pública pode mudar entre versões menores até 1.0.
- Engenharia reversa. Sem contrato público da Tesco; uma operação ou a chave pública pode mudar e quebrar uma chamada até ser atualizada.
- Autenticação precisa de um navegador real em conexão residencial. IPs de datacenter são bloqueados para login; o plano de dados HTTP puro roda em qualquer lugar.
- A busca por nutrição é limitada, não abrange o catálogo: filtra dentro de uma busca por palavra-chave, limitada por
hydrate. - Faixas de retirada precisam de um
locationUuidpara a loja onde você retira.
Ética e uso
Automação de interoperabilidade de conta pessoal: sua conta, seus dados. O cliente usa por padrão 1 requisição/segundo, concorrência única e para em 429/403. Por favor, mantenha assim. Não para revenda, raspagem em massa ou operação de múltiplas contas. Este projeto não é afiliado à Tesco; "Tesco" é uma marca registrada de seu proprietário e é usada aqui apenas para descrever interoperabilidade.
Desenvolvimento
npm install
npm test # 75 tests: vitest unit + regression + smoke
npm run build # clean build to dist/
npm run example:lookup
PRs são bem-vindos. Mantenha o código legível e mínimo, adicione um teste para qualquer mudança de comportamento e nunca faça commit de uma sessão ou chave de API.
Licença
MIT © Toby Andrews