I built a job postings search API that returns title, company, location, salary, and a dozen other fields as clean JSON. Looked perfect in testing. Then I started actually using it as an agent — comparing offers, filtering by comp, alerting on new roles — and realized half the salary field was noise.
Here's what I found when I started treating the salary data as something to reason over rather than something to display:
The same role, three listings, four salaries. "Senior Engineer, Berlin" appeared from the same company with ranges of €70–90k, €85–110k, and one posting with no salary at all. The dedup key I'd chosen (title + company) was fine, but the data wasn't. I ended up clustering by normalized title + company + location and treating salary as a distribution, not a point.
'Competitive' is a value. About 30% of postings put a string where the schema expected a number. My first instinct was to drop those rows. That threw away a third of the market. Now I parse what I can, flag the rest, and let the caller decide.
Currency is a lie until you normalize. I saw "£60k", "60000 GBP", "60,000£", and "€60.000" — European decimal comma, attached symbols, pre-tax vs gross vs TC labels buried in the description. I added a normalization pass that extracts amount + currency + period (annual/monthly/hourly) and converts to a common annual figure. It's still wrong sometimes, but it's consistently wrong in a way I can filter on.
The description is the real salary field. The structured salary is often a placeholder. The actual number usually lives in a sentence like "compensation up to €95k depending on experience." I added a light extraction pass that pulls candidate figures from the description and marks them as estimated: true so downstream agents don't treat them as authoritative.
This changed how I think about APIs that return "structured data." The structure is the easy part. The meaning — what a salary range actually represents, who posted it, when it was last updated, whether it's gross or net — is where the value is. My API now returns the raw fields plus a confidence hint and a source_note. It's messier. It's also actually useful.
I packaged the whole thing as a single endpoint that takes keywords + locations and returns normalized, deduplicated postings: https://x402.freeq.one/tools/jobs.html
If you're building agents that make decisions over scraped or aggregated data, spend the time on the normalization layer. The schema is not the product. The interpretation is.
Top comments (0)