DEV Community

Radar Labs
Radar Labs

Posted on

What 259 companies actually publish as pay: 6,823 job postings, normalised to USD

I scraped 259 companies' job boards this week: 15,532 live postings after de-duplication. 6,823 of them state an annual salary range — 43.9%. I normalised all of them to USD and put the whole thing in a free CSV, because the numbers turned out more interesting than the tool I built to collect them.

Here is what they say.

The distribution

Midpoint of the published range
p25 $154,700
median $204,750
p75 $260,630
p90 $335,000

29.9% of the salary-bearing postings are remote.

A warning before anyone quotes that median: this is a census of published ranges, not a survey of what everyone earns. Companies that publish pay skew towards US tech, and towards senior roles. The 259-company pool was itself built by testing 527 slugs and keeping the 259 that answered, so it over-represents companies with a modern ATS and an explicit pay-transparency policy. Treat $204,750 as "the middle of what these 259 companies chose to write down", not "the middle of the market".

Who publishes, and who does not

The four boards I read behave very differently:

  • Ashby returns salary as structured numbers (minValue / maxValue / currencyCode / interval). About 52% of its postings carry one. Nothing to guess.
  • Greenhouse has a metadata field, but with content=false it comes back empty for every single posting. The salary is loose text inside the job body instead — and roughly 44% of postings have one. You have to read the description and find the range.
  • Lever has a clean salaryRange object, and 9% of postings use it.
  • Workable answered for 4 companies and published no pay at all.

So of the 6,823 paid postings: 4,058 came from Greenhouse, 2,609 from Ashby, 156 from Lever.

That matters if you are choosing where to look. If you want money data, Ashby is the cheapest source per row. If you want volume, Greenhouse wins on postings but you pay for it in parsing.

Highest published medians (companies with ≥3 paid postings)

161 companies cleared the three-posting bar. The top of the list is not subtle:

Company Postings p25 Median p75
anthropic 568 $287,500 $347,500 $402,500
openai 631 $248,000 $307,500 $366,000
cohere 77 $204,400 $259,150 $286,525
perplexity 106 $195,000 $247,500 $312,500
vercel 71 $195,500 $245,000 $260,000
render 36 $230,000 $242,000 $259,000
figma 81 $207,000 $236,000 $264,500
modal 40 $200,000 $231,250 $275,000
vanta 47 $212,000 $229,000 $264,500

These medians mix seniority levels. A company hiring mostly staff engineers looks expensive; a company hiring support staff looks cheap. It is a composition effect as much as a pay effect. The first two rows are also the two with the most postings, which is a coincidence I would not read too much into — but it does tell you where the AI labs are setting the ceiling right now.

By role family, with at least 20 postings each:

Family Postings Median midpoint
product 255 $225,500
engineering 2,677 $225,000
data / ml 430 $221,337
finance / legal 229 $202,500
security 109 $195,000
design 142 $185,000
marketing 335 $179,000
sales 697 $160,000
support / ops 430 $158,875

Product managers and engineers land within $500 of each other, which surprised me. Sales spreads widest in absolute terms, because commission-heavy roles publish base ranges that mean very little on their own.

The parsing is where the lies live

Every one of these numbers went through a normaliser, and the normaliser produced three spectacular wrong answers before I made it stricter. All three are real examples from this dataset:

  1. A monthly range labelled as USD. One Lever posting had {min: 128000, max: 176000, currency: "USD", interval: "per-month-salary"}. Multiply by twelve and it becomes $1,536,000 – $2,112,000 for a DevOps engineer. That number went to the top of my leaderboard and I nearly published it.
  2. The word "hourly" anywhere in the description. A Greenhouse job description said "the base salary range is 210,000 USD - 310,000 USD" in one paragraph and mentioned "hourly employees" in another. My annualiser saw the word and multiplied by 2,080: $436,800,000.
  3. A genuinely hourly contract. $20.67 – $21.88 went in as an annual salary, because the guard at the time only rejected values below $100.

The fix that finally held was unglamorous: read the currency from the end of each number, not the start, only apply an hourly multiplier when the range actually looks hourly (floor under $500), and then throw away anything outside $5,000 – $5,000,000 after normalisation instead of trying to repair it.

The last rule costs me rows. GoCardless and Coinbase both published monthly figures I could not prove were monthly, so those rows simply have no salary now. Empty beats confidently wrong. If someone makes a career decision off this CSV, the only acceptable failure mode is a blank cell.

What is in the file

salary-ranges.csv — 1,460 rows, stratified so every company in the pool appears at least once. One row per posting:

company, ats, title, location, remote, role_family,
salary_usd_low, salary_usd_high, salary_usd_mid, posted_at, url
Enter fullscreen mode Exit fullscreen mode

CC BY 4.0. No signup, no email gate:

Captured 2026-10-24. Job boards rotate slugs constantly, so expect a handful of companies to 404 next week — the tool reports those in failedCompanies rather than silently returning less.

Why I am giving it away

I built a paid actor that returns this data live (https://apify.com/contributory_garnet_hyz/ats-job-feed, $0.0015 per job), and the honest situation is that nobody knows it exists yet. A CSV with a real number in it is a better introduction than another landing page.

If you want the refreshed version rather than the snapshot: set useVerifiedPool: true and salaryOnly: true on that actor and it returns the same schema for the current day, 259 companies, 259 HTTP requests, about four minutes.

Two things I would genuinely like feedback on:

  1. Is the midpoint the right summary? Right now I rank by it, but a $150k–$350k range and a $240k–$260k range have the same midpoint and are not the same offer.
  2. Should I keep rows where the range came from free text rather than a structured field? It costs a 144 MB download to read Greenhouse descriptions and it is where most of the garbage lived.

If you spot a salary in that CSV that is obviously wrong, tell me — I would rather fix the parser than defend it.


Numbers revision (2026-10-24): the pipeline was merging distinct city-level requisitions under one title, so row counts here are lower than the current dataset. After fixing the dedupe key the same 259 companies yield **18,567 postings / 7,984 with published pay* (median $200,000). The current figures live on the dataset page; the bug is written up here.*


Numbers revision (2026-10-24, role classifier fix)

The role-family buckets in the earlier numbers came from a single-tier classifier
that matched engineer before anything else, so Sales Engineer, Solutions
Engineer and Support Engineer were all counted as engineering
, and the word
data pulled data-center technicians into data/ML. That is also why other
reached 21.5% of rows.

Rebuilt with a four-tier classifier (disambiguation -> specific -> generic ->
broad) and 21 unit tests. Same 8,041 rows; only the buckets moved:
engineering 39.7% -> 27.4%, sales 10.8% -> 15.4%, support / ops 6.3% -> 15.2%,
finance / legal 3.3% -> 9.1%, other 21.5% -> 8.5%.

Consequence for the remote-vs-on-site gap: engineering was -11.5%; after the
fix it is -6.2% (remote median $212,000 vs on-site $226,000, n=563 / 1,638).
The direction holds, the magnitude shrinks. Current numbers:
https://sjdfi0wjvosdju9.github.io/salary-data.html

Top comments (0)