šŸ‡®šŸ‡Ŗ Tesco Ireland Example solution JavaScript /products API

Tesco Ireland vs Tesco GB: pricing the same basket on both sides of the Irish Sea

This compares prices between Tesco Ireland and Tesco Great Britain by writing one shopping list, parsing it once, and asking both chains what they would charge for it. The script pairs the two answers item by item and shows which market is cheaper — reporting figures in both EUR and GBP.

Run this yourself

$ PEPESTO_API_KEY=your_key node tesco-ie-vs-gb-price-gap.js

Full script: tesco-ie-vs-gb-price-gap.js. You'll need an API key to run it — get one here.

Getting started

If you have already checked out the GitHub repository:

bash
export PEPESTO_API_KEY=your_key_here
node tesco-ie-vs-gb-price-gap.js

One basket, priced twice

The hard part of a cross-border comparison is not fetching the prices, it is deciding what counts as the same product. Tesco Ireland's own-brand milk and Tesco GB's own-brand milk are different SKUs with different names, so there is nothing to join on.

The way round it is to let Pepesto do the deciding. Write the basket as plain generic names — "milk", "butter", "spaghetti" — send it to /parse once, and you get back a single kg_token: Pepesto's reading of what you asked for. Send that same token to /products for each chain and both come back with the same list of items, each resolved to whatever that chain actually stocks. The item_name on each entry is what you pair on.

js
const BASKET = [
  'milk',
  'butter',
  'cheddar cheese',
  'eggs',
  'chicken breast',
  'minced beef',
  'spaghetti',
  'basmati rice',
  'tinned tomatoes',
  'olive oil',
  'potatoes',
  'onions',
  'carrots',
  'bananas',
  'apples',
  'orange juice',
  'white bread',
  'porridge oats',
  'tea bags',
  'coffee',
];

/**
 * Turns the shopping list into a kg_token. The token is Pepesto's reading of
 * what was asked for, so sending the same one to both chains is what lets the
 * two baskets line up item by item.
 */
async function parseBasket() {
  console.log(`Parsing a ${BASKET.length}-item basket...`);
  const res = await fetch(`${BASE_URL}/parse`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ recipe_text: BASKET.join('\n') }),
  });
  if (!res.ok) throw new Error(`/parse failed: ${res.status}`);
  const { kg_token } = await res.json();
  return kg_token;
}

/** Prices the basket at one chain, as a map of item name → cheapest match. */
async function priceBasketAt(kgToken, domain) {
  console.log(`Pricing the basket at ${domain}...`);
  const res = await fetch(`${BASE_URL}/products`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ recipe_kg_tokens: [kgToken], supermarket_domain: domain }),
  });
  if (!res.ok) throw new Error(`/products ${domain} failed: ${res.status}`);
  const data = await res.json();

  const prices = {};
  for (const item of data.items ?? []) {
    const cheapest = (item.products ?? [])
      .filter(p => typeof p.product?.price?.price === 'number')
      .sort((a, b) => a.product.price.price - b.product.price.price)[0];
    if (!cheapest) continue;

    prices[item.item_name] = {
      name: cheapest.product.product_name || 'Unnamed product',
      price: cheapest.product.price.price,
    };
  }
  return prices;
}

Keep the list to 30 items or fewer — that is what /parse takes in one shopping list. Go over and the call fails.

Each item comes back with several candidate products, sorted with the most appropriate first. Taking the cheapest of them is what makes this a price comparison rather than a brand comparison:

json — tesco.ie /products (excerpt)
{
  "currency": "EUR",
  "items": [
    {
      "item_name": "Spaghetti",
      "products": [
        {
          "product": {
            "product_name": "Hearty Food Co. Spaghetti Pasta 500g",
            "category": "Spaghetti",
            "quantity": { "grams": 500 },
            "price": { "price": 59, "promotion": {} },
            "product_id": "https://www.tesco.ie/shop/en-IE/products/297844134"
          },
          "session_token": "eyJwcm9kdWN0X25hbWUiOiJIZWFydHkgRm9vZCBDby4g…",
          "num_units_to_buy": 1
        }
      ]
    }
  ]
}
json — tesco.com /products (excerpt)
{
  "currency": "GBP",
  "items": [
    {
      "item_name": "Spaghetti",
      "products": [
        {
          "product": {
            "product_name": "Hearty Food Co. Spaghetti Pasta 500g",
            "category": "Spaghetti",
            "quantity": { "grams": 500 },
            "price": { "price": 28, "promotion": {} },
            "product_id": "https://www.tesco.com/shop/en-GB/products/297844134"
          },
          "session_token": "eyJwcm9kdWN0X25hbWUiOiJIZWFydHkgRm9vZCBDby4g…",
          "num_units_to_buy": 1
        }
      ]
    }
  ]
}

Same item_name, same product, same 500g pack, and Tesco's own product ID is even identical across the two sites. 59 cents in Ireland, 28 pence in Britain. That one needs no interpretation at all.

What the data showed

Of the 20 items in the basket, 18 came back from both chains. EUR and GBP have been close to parity, so the cent and pence figures are directly comparable for this exercise.

Britain won 15 of the 18. Not the result I expected, and the margin is not small: Tesco loose brown onions are 11p in GB against 61 cents in Ireland, loose bananas 16p against 33 cents, six barn eggs Ā£1.00 against €1.59. The spaghetti above is the cleanest case of all — literally the same pack, at roughly half the price.

Ireland took three: cheddar, butter and porridge oats. The cheddar is the interesting one, €1.60 for a 200g block of Tesco Irish Mature Red against Ā£2.45 for a 400g Creamfields — so per kilo Ireland is not actually ahead, and the win is an artefact of pack size rather than a real saving.

Where the numbers mislead

Two things are worth knowing before quoting any of this at a dinner party.

Loose produce is priced per typical unit, and the units are not the same on both sides. Those onions come back as 211g in Ireland and 116g in Britain. Both are honestly one onion, but comparing 61 cents to 11 pence quietly compares an onion nearly twice the size. Divide by quantity.grams before you believe a gap on anything sold loose.

The other is that cheapest-match is not always like-for-like. Ask for olive oil and Ireland offers a 250ml bottle of Tesco extra virgin at €3.25, while Britain's cheapest match is a 190ml can of Frylight spray at Ā£1.75. Both are reasonable answers to "olive oil" and they are not the same product. Each item returns several candidates, so filtering on quantity or on classification before picking gives a fairer pairing than taking the cheapest outright.

The result

One /parse call and two /products calls — about 40 cents of credits — for 18 matched items and a scorecard. Britain cheaper on nearly everything, though pack sizes explain more of the gap than the headline suggests.

What else you could do?

Extend to Lidl IE vs Lidl GB, where own-brand dominance makes the comparison cleaner. Normalise everything to price-per-100g or price-per-litre using the quantity object before comparing, which removes the pack-size distortion described above. Run the diff weekly and email items where the gap moved more than 10% — "these 5 items shifted this week" is more actionable than the full table.

Links

Ready to build?

Start comparing Tesco IE and Tesco GB prices

Price one basket at Tesco Ireland and Tesco GB and see which side of the border comes out ahead.

27supermarkets 13countries 1schema Instant access