Scraping a public profile is not hard. Scraping the right number off a public profile is hard, and it fails silently, which is worse.
The failure mode is always the same shape. You find a field called something like follower_count. It holds a number. The number is in a reasonable range. You ship. Six weeks later someone notices your dashboard says an account has 527,020 followers and the account says 556,445, and you have no idea when it started being wrong, because nothing ever threw.
I've built the parser for ten platforms. Here is every near-miss I hit, with the field names.
Pinterest: 42 candidates, one right answer
follower_count appears 42 times in the HTML of a single Pinterest profile page. Most of those occurrences belong to boards, not the profile.
On the account I used for calibration, the profile's real count is 556,445. The two nearest neighbours in the document are 527,020 and 526,128 — both board follower counts. A regex that grabs the first match, or the largest, or the one nearest the top, gets a number that is off by 5% and looks completely fine.
The fix isn't a better regex. It's not using a regex: the page carries a ld+json block of type ProfilePage, and the profile's own count lives there, structurally, with no siblings to confuse it.
Rule: if the page offers structured data, parse the structure. Text proximity is not a data model.
SoundCloud: one letter
SoundCloud's hydration blob is real embedded JSON — __sc_hydration — so the structural rule above already saves you. It does not save you from this:
followers_count ← the one you want
followings_count ← one letter away, same object
Seven counters live in that object. On the account I calibrated against, followings_count is zero. So the typo doesn't produce a weird number you'd catch in review. It produces zero followers, on a major artist's account, which reads like the account is broken rather than like your parser is.
Medium: three fields, all ending in Count
Same object, three fields:
followerCount
followingCount
collectionFollowingCount
If you're matching by suffix — anything ending in Count, take the first — you get whichever one the serializer happened to emit first. Read by exact name or don't read at all.
Medium has a second trap that's worse. It does not 404. A handle that doesn't exist returns HTTP 200 with about 80 KB of account-not-found page. Your parser finds no counter, returns null, and your job logs a successful run with an empty result. You need an anchor field whose absence you treat as a hard failure, not as a zero.
Mastodon: three quarters of the feed isn't theirs
Pull a Mastodon account's recent posts and 15 out of every 20 items are boosts — other people's posts, repeated. That alone is a filtering problem you'd catch. The part you wouldn't catch:
The outer object of a boost arrives with empty text and zeroed metrics. So if you don't filter boosts, you don't get someone else's post text in your database. You get blank rows with zero engagement, attributed to the account you were scraping. Your average engagement per post collapses and nothing in the pipeline says why.
Bluesky has the same structure at a far lower rate — about 1 in 20 — which is arguably more dangerous, because a bug at 5% doesn't look like a bug. It looks like a quiet week.
Two more Mastodon specifics, since almost nobody gets them right: the handle is always user@instance — it's a federated network and a bare name identifies nobody. And the account's id is local to whichever server you asked. The same person has a different numeric id on every instance that's ever seen them. The only stable identifier is the ActivityPub URI.
Linktree: the position field is not a position
Linktree exposes a position field on each link. It reads like a rank. It is not one.
On one account with seven visible links, the position values are:
1, 28, 33, 34, 35, 41, 60
That's a sparse sort key — a gap-based ordering of the kind you get when a UI lets people drag rows around without renumbering. Sort by it and you'll get the right order by luck. Display it, or reason about "the 28th link," and you're publishing a number that means nothing.
The order the links arrive in is the order the owner chose. Use arrival order; ignore the field that's named after the thing you want.
GitHub: a repo is not a post, a star is not a like
The temptation with GitHub is to normalize it into the same shape as a social platform: public_repos → post count, stargazers_count → likes. Both mappings are wrong, and both produce numbers that will sit in a comparison chart looking authoritative.
A repository isn't a post — it's a container that gets edited for years. A star isn't a like — people star things to bookmark them, and the two behaviours have completely different base rates. If you flatten those into a cross-platform "engagement" column, the column is fiction.
I return posts_count: null for GitHub on purpose, and I keep repo fields under their source names — stargazers_count, forks_count, open_issues_count — rather than renaming them into a shape they don't fit.
Threads: the request itself is the trap
Threads is the one where the parsing is fine and the fetch is the problem.
A plain curl against a Threads profile returns about 268 KB — and that 268 KB does not contain follower_count at all. Fetch the same URL with proper TLS fingerprinting and you get about 960 KB, with the object in it.
So the naive version doesn't return a wrong number. It returns nothing, and if your code treats "field missing" as "zero" — which a surprising amount of scraping code does — you now have an account with no followers.
Also: Threads never publishes a quote count. I return quotes: null rather than 0, because those are different claims and only one of them is true.
What I return as null, and why that's the feature
Across the catalogue there are fields I could populate and deliberately don't:
verifiedon Pinterest — Pinterest has three different badges (domain_verified,is_verified_merchant,verified_identity). Picking one and calling it "verified" publishes a badge the platform didn't award.verifiedon Mastodon — Mastodon verifies links, not identity. Different claim.verifiedon Medium — the closest field isverifications.isBookAuthor, which says the person published a book. Not the same thing.followingandposts_counton Threads — the profile doesn't expose them.titleandpositionon Mastodon posts — a status has no title, and chronological order is not a ranking. Note the contrast with Linktree above: same field name, opposite problem. There it's a sparse key pretending to be a rank; here there's no rank to give, so it stays null.
A null in an API response is information: it says this platform does not answer that question. A zero says the platform answered, and the answer was none. Providers that flatten the first into the second are the reason cross-platform data comparisons don't reproduce.
The general rule
If you're building this yourself, the four things that would have saved me the most time:
- Anchor on a field whose absence is fatal. Not a count — counts can legitimately be zero. Pick a field that must exist on a real profile and treat its absence as an error, no matter what HTTP status came back. Medium returns 200 for handles that don't exist; TikTok returns 200 for challenge pages with no user on them.
- Parse structure, never proximity.
ld+json, hydration blobs, embedded state objects. If you're counting characters from a match, you've already lost. - Read fields by exact name. Never by prefix, suffix, or "the one that looks right."
followings_countis one letter fromfollowers_countand holds zero. - Null and zero are different claims. Decide which one you mean, every time.
Why I'm publishing the trap list
Because it's the actual work. The HTTP request is ten lines. The eighteen months of "wait, why is this number slightly wrong" is the product, and it doesn't show up on anyone's pricing page.
HonestHook covers ten platforms across 19 endpoints, every one with the near-misses above already ruled out, returning the same shape regardless of which platform answered. 1,000 credits a month free, no card. Pricing is here, generated from the same catalogue the API serves from.
If you'd rather build it yourself, take the list above with you. I'd genuinely rather you got the right number from your own code than the wrong number from anyone's.
Field names, counts and sample values in this post were taken from live calibration runs between 10 and 15 September 2026, one to three accounts per platform. Platforms change their internals without notice; re-verify before you rely on a specific field name.