HonestHook

Sign in

Blog ·

The field next to the right one

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:

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:

  1. 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.
  2. Parse structure, never proximity. ld+json, hydration blobs, embedded state objects. If you're counting characters from a match, you've already lost.
  3. Read fields by exact name. Never by prefix, suffix, or "the one that looks right." followings_count is one letter from followers_count and holds zero.
  4. 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.

Trend data with a memory

Every social API answers what’s trending now, then throws it away. HonestHook keeps the hourly archive, so you can ask what gained traction.

Free key, 1,000 credits a month, no card →