Labsco
tobyandrews1985 logo

Basketeer

β˜… 2

from tobyandrews1985

Drive a personal UK Tesco grocery account: search, basket, delivery slots, orders, and on-pack nutrition. Filter and rank products by macros + micros. Catalogue and nutrition tools need no auth.

πŸ”₯πŸ”₯πŸ”₯βœ“ VerifiedAccount requiredNeeds API keys
<div align="center">

basketeer

A typed, pure-HTTP TypeScript SDK for your own Tesco grocery account, with on-pack nutrition normalized into typed data.

Run your weekly shop from code, the terminal, or an AI agent. Everything but sign-in and payment is plain fetch, and products come back with their on-pack nutrition normalized into typed macros and micronutrients you can search and rank by.

<sub>Unofficial Β· not affiliated with Tesco Β· for automating your own account Β· MIT</sub>

<br> <img src="https://raw.githubusercontent.com/tobyandrews1985/basketeer/main/docs/media/nutrition.gif" alt="basketeer filtering and ranking a live Tesco search by nutrition, then reading a product's micronutrients" width="760">

<sub>Filter and rank a live search by on-pack nutrition (protein β‰₯ 10g, sugar ≀ 7g), then read any product's full macros and micronutrients. Real data, no login.</sub>

<br><br>

<img src="https://raw.githubusercontent.com/tobyandrews1985/basketeer/main/docs/media/demo.gif" alt="basketeer searching Tesco from the CLI, live results, no browser" width="760">

<sub>Plain catalogue search is a one-liner: <code>basketeer search "oat milk"</code> piped to <code>jq</code>. Real, live, no login.</sub>

<br>

CI tests license: MIT node >=18

</div>

Why basketeer

Tesco has no public API, and the usual approach (scraping the DOM) shatters on the next site redesign, stops at titles and prices, and can't be driven by an AI agent. basketeer talks to Tesco's GraphQL gateway directly:

  • Nutritionally aware. Where Tesco lists on-pack nutrition, basketeer normalizes it into typed macros and structured micronutrients, free on anonymous reads, and lets you filter and rank a search by them (searchByNutrition). A first-class API, not a scraped afterthought.
  • Robust. Pure-HTTP GraphQL, not DOM scraping. A cosmetic site redesign won't break it.
  • Complete. Book, amend, cancel, and reorder a delivered shop. The full order lifecycle, not just "add to basket."
  • Agent-ready. A stdio MCP server lets Claude or any MCP client run the shop. Read-only and destructive tools are annotated, and checkout never pays.
  • Typed and lean. One fully-typed client you import; the CLI and MCP server are built on it. The data path imports no third-party packages (the three runtime deps β€” commander, the MCP SDK, and zod β€” are pulled only by the CLI and MCP server). It is pure fetch with no Node-only APIs, so it runs on Node and Node-compatible runtimes.
  • Safe. checkout() stops at the payment URL. A human finishes 3-D Secure in a browser, by design.
  • Tested. 75 tests across the data plane and its parsers.

Nutrition, the part nothing else has

When Tesco lists a product's on-pack nutrition, basketeer normalizes it into typed macros (energy, protein, fat, saturates, carbs, sugars, fibre, salt) and structured micronutrients (a named entry per vitamin and mineral, with amount, unit, and % of the Nutrient Reference Value). Free, on anonymous reads (nutrition is null when a product has no usable rows). And you can search and rank by it:

# "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 }, ...]

Nutrition-filtered search runs a keyword search, then fetches each candidate's nutrition (one throttled product call each, capped by hydrate, default 20) and filters locally. It filters within a search; it does not scan the whole catalogue. hydrated/failed report the exact cost.


[!IMPORTANT] Not affiliated with, endorsed by, or connected to Tesco. This is an unofficial, reverse-engineered client for automating your own account, in the spirit of personal interoperability. It can break if Tesco changes their API. Use it for your own shopping, at your own risk, within Tesco's terms. Not for resale, scraping at scale, or operating accounts that aren't yours. See Ethics & usage.

Capabilities

The full grocery lifecycle, typed end to end:

  • Nutrition β€” typed macros and structured micros, normalized from a product's on-pack rows when present; filter and rank a search by nutrition (anonymous)
  • Catalogue β€” search, getProduct, browseCategory (anonymous); favourites / "my usuals" (authed)
  • Product images β€” imageUrl on every product/result; resizeImageUrl(url, { width, height }) for thumbnails (anonymous)
  • Basket β€” add, set, remove, get
  • Slots β€” delivery and collection: list / book / release
  • Orders β€” list, amend, cancel, lastFulfilled (reorder)
  • Checkout β€” checkout() returns the payment URL; it never pays

β†’ Full reference (signatures, return types, the error catalogue, and where the browser runs): docs/api.md

How it works

Tesco's website talks to a GraphQL gateway at xapi.tesco.com. basketeer speaks that protocol directly.

  • The data plane is pure HTTP. Search, product, basket, slots, and orders are GraphQL operations over plain fetch. Stateless, no browser, throttled to a polite 1 req/s, with a hard stop on 429/403 (no retry-storms). Anonymous reads (search, product, browse, nutrition) need only the public x-apikey; favourites, basket, slots, and orders need a session.
  • A browser is needed only for auth. Sign-in is guarded by Akamai (TLS fingerprinting plus a JS challenge) that only a genuine browser satisfies. BrowserAuthBackend drives your installed Google Chrome to sign you in once and harvests the session (an OAuth.AccessToken bearer plus cookies). The access token lasts about an hour and refreshes via the same browser path; the underlying session lasts about 30 days.
  • Payment is deliberately out of scope. Paying goes through a separate, CSRF-protected checkout app and 3-D Secure card authentication. That is browser-bound and fraud-sensitive by nature. checkout() returns the current basket and the URL where you finish payment; you fill the basket and book a slot with the earlier calls, and checkout() itself only hands off. basketeer never pays.

Auth, you choose where the browser runs

The library hard-depends on no browser. It just needs a Session. Run the browser on the user's machine (BrowserAuthBackend plus the optional playwright peer), under Xvfb in a long-running container, on a residential hosted browser for serverless, or skip it entirely and hand in cookies you harvested yourself:

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

Implement your own backend with the two-method AuthBackend (login, refresh) and three-method TokenStore (load, save, clear). FileTokenStore and MemoryTokenStore ship in the box. The full host matrix is in docs/api.md.

Serverless note. A serverless function can't hold a browser, and Tesco's Akamai blocks sign-in from datacenter IPs, so a hosted browser needs a residential egress. Off-the-shelf managed-browser proxies (Browserbase and similar) are also commonly blocked for supermarket domains. The dependable pattern is a browser on a residential connection you control (a home server, a Pi, the user's device), with the pure-HTTP data plane running anywhere.

Orders & amend

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");

MCP server (for AI agents)

A stdio MCP server ships as the basketeer-mcp bin, exposing tools (basketeer_search, basketeer_search_by_nutrition, basketeer_nutrition, basketeer_basket_set, basketeer_slots_list, basketeer_orders_list, basketeer_checkout, …) so Claude Desktop or any MCP client can shop. Read-only tools carry readOnlyHint; mutating ones carry destructiveHint, and basketeer_orders_cancel / basketeer_checkout take a two-step confirm token. basketeer_checkout returns the payment URL for the human. There is no "pay" tool.

// 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

The basketeer bin prints JSON to stdout, coded errors to stderr. Install globally for the bare command, or prefix with 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

Examples

Runnable scripts in examples/: lookup.ts (anonymous), login.ts, shop-flow.ts (search β†’ basket β†’ slot β†’ checkout handoff), orders.ts, and bring-your-own-auth.ts.

Security & session storage

FileTokenStore writes ~/.basketeer/session.json containing a bearer token and cookies in plaintext. Treat it like a password: keep its file permissions tight, never commit it, and clear it (store.clear()) on a shared machine. For ephemeral or server contexts use MemoryTokenStore or your own TokenStore, and keep the session out of logs.

Development

npm install
npm test          # 75 tests: vitest unit + regression + smoke
npm run build     # clean build to dist/
npm run example:lookup

PRs welcome. Keep code readable and minimal, add a test for any behaviour change, and never commit a session or API key.

License

MIT Β© Toby Andrews