# Salary Benchmarks — Live Pay Bands from Job Postings (`alaudinburki/salary-benchmarks`) Actor

Extract salary bands from live remote job postings: min, max, midpoint, currency and an annualised figure. Reports coverage honestly — only postings that disclose pay become rows.

- **URL**: https://apify.com/alaudinburki/salary-benchmarks.md
- **Developed by:** [alaudin burki](https://apify.com/alaudinburki) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Salary Benchmarks — Live Pay Bands from Job Postings

Salary bands from **live remote job postings**: minimum, maximum, midpoint, currency and an
annualised figure — from two keyless feeds (Jobicy and RemoteOK).

### The number most salary datasets hide

**Only 48 of 149 current postings disclosed pay.** Most job ads do not publish a salary, so a tool
that quietly averages whatever it found is reporting on a third of the market and calling it "the
market". This Actor puts the coverage in `SUMMARY` beside the rows:

```
postingsFetched: 149 · postingsWithSalary: 48 · coveragePct: 32
```

You always know what share of the data you are looking at.

### Three rules it will not break

**1. Currency is never converted.** A `USD 190,500` role and a `PLN 240,000` role are not comparable
by multiplying, and an exchange rate applies today's guess to next month's data. Each row carries its
own currency; `SUMMARY.byCurrency` gives a median **per currency**, and comparisons happen there.

**2. Annualising states its assumption.** Sources post yearly, monthly and hourly pay. To compare them
the Actor converts to a yearly figure and writes the assumption on the row:

```json
{ "period": "hourly", "annualised": true,
  "annualAssumption": "2080 hours per year for hourly; 12 months for monthly" }
```

The raw `salaryMin` / `salaryMax` are kept beside the derived `annualMin` / `annualMax`, because an
hourly contract without paid leave is not literally 2,080 hours.

**3. A midpoint needs both ends.** A posting with only a floor has no midpoint, so `annualMid` is
`null` — not the minimum restated as if it were an average.

### Real output

| title | currency | salaryMin | salaryMax | annualMid |
|---|---|---|---|---|
| Senior Finance Manager | USD | 190,500 | 214,767 | 202,634 |
| Product Manager, Onboarding | USD | 180,000 | 260,000 | 220,000 |
| Business Development Rep | USD | 70,000 | 75,000 | 72,500 |

### Use cases

- **Candidates** — what a role actually pays before the first call.
- **Recruiters** — a defensible range instead of a guess.
- **Compensation analysts** — a live sample, refreshed on a schedule, with the coverage figure attached.
- **Job boards** — which roles publish pay and which do not, by company.

### Limits

- **Remote-first sources.** These feeds skew remote and tech; the sample is not the whole labour market.
- **Posted ranges, not offers.** A posted band is the employer's stated intent, not what anyone accepted.
- **Coverage varies by source.** RemoteOK disclosed on 20 of 99 postings, Jobicy on 28 of 50 in one run.
- **No role normalisation.** "Senior Finance Manager" and "Finance Manager, Senior" stay separate rows;
  grouping them is analysis, not extraction.

### Verified

**36 unit tests** on the reader — every period multiplier, the one-ended range, an inverted range, the
no-salary case and the median — plus **15 live checks** against both feeds: 149 postings fetched, 48
with pay, hourly rows flagged, midpoints only where both ends exist, and every row flat.

***

One row per paying posting, highest midpoint first, with the coverage stated.

### Fields

| Field | Type | What it holds |
|---|---|---|
| `id` | string | Stable row identity across sources. |
| `source` | string | Which feed produced the row: jobicy or remoteok. |
| `title` | string | The role as posted. |
| `company` | string | The hiring company. |
| `location` | string | Where the role is based, as the source states it. |
| `level` | string | Seniority when the source publishes it. |
| `industry` | string | Industry when the source publishes it. |
| `salaryMin` | number | The bottom of the posted range, in the posted period and currency. |
| `salaryMax` | number | The top of the posted range. |
| `currency` | string | ISO currency code, or 'not stated'. Never converted. |
| `period` | string | The period the posted figures are in: yearly, monthly, hourly and so on. |
| `annualMin` | number | salaryMin annualised. Equals the raw value when the period is yearly. |
| `annualMax` | number | salaryMax annualised. |
| `annualMid` | number | Midpoint of the annual range — null when only one end was posted. |
| `annualised` | boolean | True when a non-yearly figure was converted, so the assumption is visible. |
| `annualAssumption` | string | The conversion applied, e.g. 2080 hours per year for hourly pay. Null when no conversion was needed. |
| `rangeWasInverted` | boolean | True when the source posted max below min; the row is ordered and the swap is disclosed. |
| `publishedAt` | string | When the posting was published. |
| `url` | string | The posting. |
| `scrapedAt` | string | When this run fetched it. |

# Actor input Schema

## `sources` (type: `array`):

Which feeds to read.

## `keywords` (type: `string`):

Comma-separated words matched against the title, industry and level. Leave blank for every role.

## `minAnnualSalary` (type: `integer`):

Drop rows whose midpoint is below this, in their own currency. 0 keeps everything.

## `maxItems` (type: `integer`):

Hard limit.

## `timeoutSecs` (type: `integer`):

Per source.

## Actor input object example

```json
{
  "sources": [
    "jobicy",
    "remoteok"
  ],
  "minAnnualSalary": 0,
  "maxItems": 2000,
  "timeoutSecs": 30
}
```

# Actor output Schema

## `results` (type: `string`):

One row per posting that discloses pay, highest midpoint first.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sources": [
        "jobicy",
        "remoteok"
    ],
    "keywords": "",
    "minAnnualSalary": 0,
    "maxItems": 2000
};

// Run the Actor and wait for it to finish
const run = await client.actor("alaudinburki/salary-benchmarks").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "sources": [
        "jobicy",
        "remoteok",
    ],
    "keywords": "",
    "minAnnualSalary": 0,
    "maxItems": 2000,
}

# Run the Actor and wait for it to finish
run = client.actor("alaudinburki/salary-benchmarks").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [
    "jobicy",
    "remoteok"
  ],
  "keywords": "",
  "minAnnualSalary": 0,
  "maxItems": 2000
}' |
apify call alaudinburki/salary-benchmarks --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,alaudinburki/salary-benchmarks"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/dBosKx66NAJB3X31j/builds/KdqmFAgpBdWahgVeF/openapi.json
