Scan Conad's Weekly Promotions โ Every Deal Sorted by Best Value
This pulls every discounted Conad Italy product via Pepesto's /promotions endpoint and ranks them by how deep the discount goes โ useful for building a weekly Italian grocery deals digest.
Run this yourself
$ PEPESTO_API_KEY=your_key node conad-italy-promotions-scanner.jsFull script: conad-italy-promotions-scanner.js. You'll need an API key to run it โ get one here.
Getting started
export PEPESTO_API_KEY=your_key_here
node conad-italy-promotions-scanner.jsThe promotions call
The obvious move is /catalog, which returns every product the API has indexed for a chain. It works, but it hands back thousands of products when only a few dozen are on offer, and at โฌ9.90 a call it is the most expensive thing in the API. /promotions answers in exactly the same shape and returns only the discounted part of the range, for โฌ3.20. For this job there is no reason to pay for the rest.
async function fetchConadPromotions() {
console.log('Fetching Conad promotions...');
const res = await fetch(`${BASE_URL}/promotions`, {
method: 'POST',
headers,
body: JSON.stringify({ supermarket_domain: 'spesaonline.conad.it' }),
});
if (!res.ok) throw new Error(`/promotions failed: ${res.status}`);
const data = await res.json();
return Object.entries(data.parsed_products ?? {}).map(([url, product]) => ({ url, ...product }));
}The response is keyed by product URL rather than being a list, which is why it gets flattened on the way out. Each product carries Italian and English names, the price in EUR cents, both a Conad image and a Pepesto-hosted one, the pack size as a structured quantity, and โ when Conad publishes it โ the discount percentage:
{
"parsed_products": {
"https://spesaonline.conad.it/p/arancia-500-ml-conad--11174085": {
"names": {
"en": "Conad Orange Juice 500 ml",
"it": "Arancia 500 ml Conad"
},
"self_hosted_image": "https://storage.googleapis.com/pepesto_recipe_images/df4facb933ef805ef0cba1623b.webp",
"remote_image": "https://spesaonline.conad.it/assets/products/arancia-500-ml-conad--11174085/ID-Shot.jpeg/renditions/medium.jpeg",
"price": 299,
"promo": true,
"promo_percentage": 50,
"price_per_meausure_unit": "5.98 โฌ / L",
"quantity": { "Unit": { "Milliliters": 500 } }
},
"https://spesaonline.conad.it/p/cannelloni-al-ragu-di-carne-500-g-conad--400992": {
"names": {
"en": "Conad Frozen Cannelloni with Meat Ragรน 500 g",
"it": "Cannelloni al ragรน di carne 500 g Conad"
},
"price": 375,
"promo": true,
"promo_percentage": 50,
"price_per_meausure_unit": "7.50 โฌ / Kg",
"quantity": { "Unit": { "HundredGrams": 5 }, "accurate_grams": 500 }
},
"https://spesaonline.conad.it/p/aceto-balsamico-di-modena-igp-500-ml-conad--335268": {
"names": {
"en": "Balsamic Vinegar of Modena IGP 500 ml Conad",
"it": "Aceto Balsamico di Modena IGP 500 ml Conad"
},
"price": 149,
"promo": true,
"price_per_meausure_unit": "2.98 โฌ / L",
"quantity": { "Unit": { "Milliliters": 500 } }
}
}
}Sorting and displaying the deals
The first version of this sorted on the number pulled out of price_per_meausure_unit, on the theory that a price per kilo compares fairly across pack sizes. That field is a display string, not a number, and four of the sixty came back looking like "12 30 โฌ / Kg" โ a space where the decimal point should be. Parsed naively, a โฌ12.30/kg mozzarella sorts as if it cost twelve euros and thirty cents per kilo separately. Print the string, do not do arithmetic on it.
Sorting on promo_percentage is both simpler and closer to what you actually want, which is the deepest discount rather than the lowest shelf price. Conad does not publish a percentage for every promotion, so those fall to the back and order by price.
// promo_percentage is the honest measure of a deal, but Conad only publishes it
// for straightforward price cuts. A "buy 2 get 3" carries no percentage, so
// those sort to the back and are ordered by price instead.
function rankByDiscount(products) {
const discountOf = p => p.promo_percentage ?? 0;
return [...products].sort(
(a, b) => discountOf(b) - discountOf(a) || (a.price ?? 0) - (b.price ?? 0),
);
}
function printDeal(p, i) {
const rank = String(i + 1).padStart(2, ' ');
const name = p.names?.en || p.names?.it || 'Unnamed product';
const price = typeof p.price === 'number' ? formatPrice(p.price) : 'price unavailable';
const discount = p.promo_percentage ? ` โ ${p.promo_percentage}% off` : '';
const qty = formatQuantity(p.quantity);
// Note the spelling: the field really is price_per_meausure_unit. It is a
// display string ("2.98 โฌ / L"), not a number, so print it rather than
// trying to do arithmetic on it.
const perUnit = p.price_per_meausure_unit ? ` @ ${p.price_per_meausure_unit}` : '';
console.log(`${rank}. ${name}${discount}`);
console.log(` ${qty ? `${qty} | ` : ''}${price}${perUnit}`);
console.log();
}What the data showed
In a sample run Conad had 60 products on promotion. That is a small enough list to read end to end, which is the point โ it is a week's offers, not a catalogue.
Seven items were at half price or better, all of them Conad's own label: lemon and peach iced tea at โฌ0.49 for 500ml, zero-sugar soy milk at โฌ0.99 a litre, salted crackers at โฌ1.29 for 500g, whipping cream at โฌ1.79, orange juice at โฌ2.99, and frozen cannelloni with meat ragรน at โฌ3.75. Below that, Yomo strawberry yoghurt at 45% off and a nine-tin pack of Conad tuna in olive oil at 37% off were the two that looked worth stocking up on.
The bulk of the list sits in the middle: six items between 30% and 49% off, twenty between 15% and 29%. Only three were shallower than 15%, which suggests Conad does not bother flagging small reductions as promotions at all.
The awkward part is the other twenty-four. They come back with promo: true and no promo_percentage at all โ roughly two in five of the list. Some of those are multi-buys rather than price cuts, where a single percentage would not mean much anyway. Count them separately instead of letting them default to zero, or the summary quietly reports two fifths of the week's offers as no discount.
The result
Run this every Monday morning. The output is a sorted text list that can be scanned in under a minute. Any items of interest go into a manual Conad order, or pipe the URLs straight into a /products + /session flow to pre-fill a basket with the deals.
What else you could do?
Add price history tracking โ store each week's snapshot and alert when a product goes on promo for the first time, or when a recurring deal returns after a gap. Add a watchlist so only the things you actually buy come through, rather than all sixty. Wire up a Telegram or email notification to deliver the top 10 deals each Monday morning automatically.