# Google Shopping SERP capture protocol

Reusable for all 10 flagship queries. Run in a NORMAL browser, not automation.

## Why this exists

An automated capture on 2026-09-02 found all 39 product tiles for
"best moisturizer for dry skin" sitting inside a "Sponsored Products" container, with zero
organic tiles. Google can serve different layouts to automation-suspected sessions, so this
must be confirmed by a human in a real browser before anything is published.

## Steps

**1. Record your starting state.**
Note the date, time (with timezone), your city, and which browser. Google Shopping is
personalised by location and account. These go in the methodology block.

**2. Open the query.** Paste this URL directly. Do not search from the Google homepage — the
URL pins the Shopping surface.

    https://www.google.com/search?q=best+moisturizer+for+dry+skin&udm=28

You should land on Google Shopping. Note if the URL rewrites (the older `tbm=shop` now
redirects to `udm=28`).

**3. Screenshot the full page.** Full-page capture, not just the viewport. This is evidence.

**4. Count "Sponsored" by hand.** Ctrl+F (Cmd+F) for `Sponsored`. Record the hit count.
The automated run found exactly **1** — a single block heading, no per-item labels.

**5. Look for where the sponsored block ends.** Scroll from the "Sponsored Products" heading
downward. The question is whether a separate, unlabeled product grid appears before the
themed carousels ("Dermatologist-Recommended Moisturizers", "Drugstore Moisturizers").
If there isn't one, the whole grid is paid.

**6. Get the hard number.** Open the console (Cmd+Option+J on Chrome / Cmd+Option+K on
Firefox). Chrome may require you to type `allow pasting` and hit enter first. Paste:

```js
const priceRe = /\$\d[\d,]*\.?\d*/;
const tiles = root => new Set([...root.querySelectorAll('a[href]')]
  .map(a => (a.innerText || '').trim())
  .filter(t => t.length > 20 && t.length < 400 && priceRe.test(t))
  .map(t => t.slice(0, 60)));

const label = [...document.querySelectorAll('*')]
  .find(e => e.childElementCount === 0 && /sponsored products/i.test((e.textContent || '').trim()));

if (!label) {
  console.log('NO "Sponsored Products" heading found — different layout than the pilot');
} else {
  let c = label, hops = 0;
  while (c.parentElement && c.textContent.trim().length < 400 && hops < 10) { c = c.parentElement; hops++; }
  const all = tiles(document.body), spon = tiles(c);
  console.log({
    totalTiles: all.size,
    tilesInSponsored: spon.size,
    tilesOutsideSponsored: [...all].filter(t => !spon.has(t)).length,
    sponsoredStringsOnPage: (document.body.innerText.match(/Sponsored/gi) || []).length
  });
}
```

Record all four numbers.

**7. Repeat in a private window.** Same URL, logged out. Compare. If logged-in and logged-out
differ, that difference is itself worth reporting.

**8. Save the brand list in page order.** Copy the page text into
`serp/qNN-google-shopping-<timestamp>.md` alongside the numbers above.

## How to read the result

| totalTiles vs tilesInSponsored | Meaning |
|---|---|
| Equal, outside = 0 | Grid is paid end to end. Pilot finding confirmed. The strongest version of the story. |
| Outside > 0 | Organic tiles exist. Pilot was an automation artifact. Original Peec replication is back on. |
| No sponsored heading | Different layout entirely. Re-screenshot and re-assess. |

## Timing rule

Capture the SERP **immediately after** that query's 5 ChatGPT runs, not in a batch at the end.
Google Shopping drifts. Tight pairing is the rigor advantage over any study comparing against
a stale index.


---

## Pagination rule (added 2026-09-02, after two bad captures)

The organic "All products" grid **paginates**. First load is 40. A "More results" control loads
more, and it keeps going — measured to 229 on one query with more still available.

**40 is a page size. It is not the grid.** Do not report it as a grid size, and do not treat a
match with a published "top 40" figure as confirmation of anything.

**Required for every capture:**
1. Record the count at first load, before any expansion.
2. Record whether "More results" was clicked and how many times.
3. Record the final count and whether "More results" was still present.
4. **State the denominator used for overlap explicitly.** Report overlap at top-10, top-40, and
   full-grid depth. A single overlap number without its denominator is not a finding.

Rank is also unstable between sessions, including two logged-out ones. **Measure set
membership; treat rank as a distribution, never as a single ordering.**

## DECIDED 2026-09-02: denominator = top 40 (first page), logged out

I's call. Matches Peec, enables direct comparison, and is a real boundary for a logged-out
user. State it explicitly in the methodology block as a chosen cut, never as "the grid".

**Session behavior differs and this matters:**
- **Logged out** — click-gated. The grid stops at 40 and requires "More results" to go further.
- **Logged in** — infinite scroll. I reached 160 by scrolling alone, no clicks.

So a logged-out user naturally stops at 40; a logged-in user scrolls past it without noticing.
Same shelf, different depth of exposure. Captures are logged-out, first page, no clicks —
which makes them deterministic and reproducible.
