Grocery prices are local. A carton of eggs can cost one price in a suburban store, another in the city centre, and a third in a delivery app that adds its own markup to the same shelf. If you price against local rivals, check a brand’s promotions or follow food inflation by region, you need prices from each city you care about, not one national average. That is a core price intelligence job, and it is harder for groceries than for most retail.
This guide explains where grocery prices vary, how delivery apps decide which store you see, and how to set up a collection that reads each city the way a local shopper does.
Where grocery prices vary
Grocers rarely run one price list. The usual layers are:
- Price zones. Chains group stores into zones by local competition and costs. Two stores of the same chain, a few kilometres apart, can sit in different zones.
- Store-level promotions. Weekly flyers and loyalty offers often differ by region, and some deals are only valid in selected stores.
- Delivery app markups. Third-party apps may show prices above the in-store shelf price, plus service and delivery fees that change by area and time of day.
- Availability. An item can be in stock in one store and missing in another, which changes what a basket really costs.
- Pack sizes and own brands. The same product line can come in different sizes by region, which hides price changes unless you compare unit prices.
A single national scrape blends all of this into one misleading number. You need the store, the city and the channel attached to every price you record.
How delivery apps and grocer sites pick a store
Most grocery sites will not show a full catalogue until they know where you are. They decide in roughly this order:
- A saved store or address, from a cookie or the account.
- The address or postcode you type into the store picker or delivery check.
- Your IP location, as a first guess before you choose anything.
That has a practical consequence. The IP gets you into the right city and makes the first guess land near the store you want. The exact store, down to the postcode, is set by the address you give the site. So a reliable setup does both: a residential IP in the target city, and the store or delivery address entered in the same session, so the two never disagree. Our post on when city-level proxy targeting matters covers why a mismatch between IP and chosen location can change what a site serves.
Building the basket
Before collecting anything, fix what you will compare:
- A fixed basket of 50 to 300 products: staples, your own top sellers and the items your rivals promote most.
- A matching key for each product, such as the barcode (GTIN) where the site exposes it, otherwise a brand, name and size match you check by hand once.
- Unit price (per kg, per litre, per unit) next to the shelf price, so pack-size changes do not look like price changes.
- A store list per city: which stores and which delivery areas represent that city for you.
Keep the basket stable. Changing it every week makes trends impossible to read.
Collecting city by city
For each city, open a session through residential proxies targeted at that city, set the store in the site, then read the basket. Keep one IP for the whole session with a sticky session ID, so the site does not see the shopper jump between locations halfway through.
customer-USERNAME-country-us-city-chicago-sid-chi-store-118-ttl-900:PASSWORD
The session ID (sid) keeps the same exit for up to the ttl in seconds. Use one ID per store, so each store visit looks like one shopper. For a large market, spread the work across its main cities, for example Chicago, Dallas and Atlanta on the United States pages, instead of reading the whole country from one place.
A minimal loop in Python looks like this:
import requests
GATEWAY = "p.shifter.io:443"
USER, PASS = "USERNAME", "PASSWORD"
def session_for(city, store_id):
s = requests.Session()
proxy = f"http://customer-{USER}-country-us-city-{city}-sid-{city}-{store_id}-ttl-900:{PASS}@{GATEWAY}"
s.proxies = {"http": proxy, "https": proxy}
return s
def read_basket(city, store_id, products):
s = session_for(city, store_id)
# 1. Set the store the way the site expects (store picker or address check).
s.post("https://grocer.example/api/store/select", json={"storeId": store_id}, timeout=30)
rows = []
for p in products:
r = s.get(f"https://grocer.example/api/product/{p['sku']}", timeout=30)
if r.status_code != 200:
rows.append({**p, "city": city, "store": store_id, "error": r.status_code})
continue
d = r.json()
rows.append({**p, "city": city, "store": store_id,
"price": d.get("price"), "unit_price": d.get("unitPrice"),
"promo": d.get("promotion"), "in_stock": d.get("available")})
return rows
The endpoints here are placeholders. Many grocer sites load prices from a JSON endpoint after the store is set; reading that endpoint is lighter and more stable than parsing the page.
Reading delivery apps
Delivery apps add two things you must record separately: the item price in the app and the fees. Capture:
- Item price in the app next to the shelf price from the grocer’s own site, to measure the markup.
- Service and delivery fees for a fixed basket value, since fees often depend on the order total.
- Time of collection, because fees and availability shift during the day.
Compare like with like: same store, same basket, same hour.
Keeping the data clean
- Store every price with city, store, channel, time and unit price. Without the store, you cannot explain a change.
- Flag promotions instead of averaging them in. A two-week deal is not a price cut.
- Watch for missing items. An empty price is often an out-of-stock, which is a signal in itself.
- Check for personalised prices. Logged-in loyalty prices can differ from guest prices. Collect as a guest unless loyalty pricing is what you study. Our guide to detecting geo-personalized and dynamic pricing shows how to separate the two.
- Collect only public prices, at a polite rate, and respect each site’s terms.
The bottom line
A grocery price only means something with its city, store and channel attached. Fix a basket, decide which stores stand for each city, enter each city through a residential IP located there, set the store inside the site in the same session, and record unit prices, promotions and fees separately. Run it weekly for staples and daily for the items you compete on, and you will see local price moves as they happen instead of one blurred national number.