Quick answer: Grailed is a Next.js site, so __NEXT_DATA__ is right there in the HTML — and props.initialProps.pageProps is empty. The search results are not server-rendered at all. They're fetched client-side from Algolia, using an application ID and a public search key that the page hands you in that same blob. Harvest the credentials once, then query Algolia's REST API directly. No browser, no HTML parsing.
The recon note was wrong, and that's fine
Our reconnaissance said Grailed server-renders listings into __NEXT_DATA__. It even had corroborating evidence: a GET /shop?query=jacket returns ~395 KB with a __NEXT_DATA__ tag present and 1,536 occurrences of "price": in the body.
Both facts are true. The conclusion drawn from them was not.
Those price strings live in filter metadata, related-product rails and SEO scaffolding — not in the search result set. The actual results array is not in the document at all. This is the trap: a page can be full of the kind of data you want while containing none of the data you asked for, and a substring count can't tell the difference.
Recon buys you a GO/NO-GO on reachability. It does not settle the wire format. The wire format gets settled by whoever writes the parser, and if they discover the note was wrong, that's the process working rather than failing.
What's actually in the blob
Not listings — credentials:
props.… → algolia application ID
algolia public search API key
That's a public, client-side search key. It's meant to be in the page; it's what the site's own JavaScript uses to run the search you'd be running. So the flow is:
-
GET /shop?query=…once per run, purely to harvestappIdand the public search key from__NEXT_DATA__. -
POSTto Algolia's REST search endpoint against theListing_productionindex, once per query and page.
Everything after step 1 is JSON in, JSON out. No HTML parsing, no CSS selectors to break when a designer renames a class, no headless browser.
Why this is the better outcome
Scraping rendered HTML means your parser is coupled to presentation. Someone ships a redesign and your div.listing-card__price selector evaporates.
Querying the search API the site itself queries means you get the same structured records its own frontend gets — stable field names, real types, proper pagination — and you're one well-formed request away from any page depth you want.
It's worth checking for this shape on any client-rendered site before you reach for a browser. The pattern — the page ships the keys to the API that holds the data — is extremely common with Algolia, and much more common generally than people expect. The credentials are public by design, because the client has to use them.
The general lesson
Two site-specific findings landed the same day for us, and they rhyme:
- Coursera embeds
window.Appand__APOLLO_STATE__, and neither carries the search results — they're server-rendered in the markup instead. - Grailed embeds
__NEXT_DATA__, which also doesn't carry the results — they come from Algolia.
One site looked client-rendered and was server-rendered. The other looked server-rendered and was client-rendered. In both cases the giveaway blob was present and misleading.
So don't infer the mechanism from the framework. Dump the blob's actual keys and look for the array you need. It takes a minute and it's the difference between a stable integration and a parser you rewrite every quarter.
🧥 Grailed Listings Scraper pulls menswear and streetwear resale listings into typed rows — listing ID, title, brand, price, currency, size and seller — across any queries and page depth you name. $5.00 per 1,000 results, and you only pay for rows that land.
FAQ
Does Grailed server-render its search results?
No. __NEXT_DATA__ is present but props.initialProps.pageProps is empty; results hydrate client-side from Algolia.
Do I need a headless browser for Grailed?
No. One plain GET harvests the Algolia app ID and public search key, and every subsequent request is a normal JSON POST to Algolia.
Is using that search key legitimate?
It is a public, client-side search key that the page serves to every visitor — the same credential the site's own frontend uses. It is not a private or admin key.
Why not just count "price": in the HTML to confirm the data is there?
Because a substring count can't distinguish result data from filter metadata, related-product rails and SEO markup. That exact check is what produced our wrong conclusion.
Top comments (0)