# Ashby Salary Monitor — Pay Range Changes & Job History (`verdant_pine/ashby-compensation-monitor`) Actor

Track published salary ranges on Ashby job boards. Get before/after ranges, new and reappeared jobs, and confirmed removals. No LLM or proxy required.

- **URL**: https://apify.com/verdant\_pine/ashby-compensation-monitor.md
- **Developed by:** [Viktor](https://apify.com/verdant_pine) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Ashby Salary Monitor

See when a company's published pay ranges change. Keep the old and new ranges, regional tiers, currencies and pay periods together with the original job link.

Designed for compensation research, recruiting teams and job-data pipelines watching a known list of Ashby employers. Also reports new listings, location changes, reappearances and confirmed removals.

**Free beta: no developer fee. Standard Apify platform usage charges apply to your account.**

The Console starts in snapshot mode so every successful check displays current jobs. Use changes mode below for a feed of differences.

### Try one board

```json
{
  "boards": ["Ashby"],
  "monitorId": "my-watchlist",
  "outputMode": "changes",
  "emitInitial": true,
  "removalConfirmations": 2
}
```

The first run returns `BASELINE` rows: listings that already existed when monitoring began. Repeat with the same watch list name to get changes. An unchanged board returns no change rows; the `SUMMARY` record still shows jobs checked and salary coverage.

Schedule daily and export the dataset to JSON, CSV or Excel, or connect its API to your reporting workflow. The Actor itself does not send emails or Slack messages.

### What you receive

| Field | Meaning |
|---|---|
| `changeType` | `BASELINE`, `NEW`, `CHANGED`, `REMOVED`, `REAPPEARED`, or `UNCHANGED` in snapshot mode |
| `changedFields` | Which tracked fields changed |
| `salaryRanges` | Separate range per tier, currency and period; no conversion or averaging |
| `salaryRangesJson` / `previousSalaryRangesJson` | CSV-friendly before/after range arrays |
| `salaryDataStatus` | `structured`, `summary_only`, or `not_published` |
| `salarySummary` | Source's salary wording, when available |
| `observedAt` / `previousObservedAt` | Observation times, not claimed publication or closure times |
| `jobUrl` | Original public posting |
| `eventId` | Identifier for this observed event |

Example of a **synthetic** salary change:

```json
{
  "changeType": "CHANGED",
  "title": "Software Engineer",
  "changedFields": ["salaryRanges"],
  "previousSalaryRangesJson": "[{\"tier\":\"EU\",\"currency\":\"EUR\",\"interval\":\"1 YEAR\",\"min\":100000,\"max\":140000}]",
  "salaryRangesJson": "[{\"tier\":\"EU\",\"currency\":\"EUR\",\"interval\":\"1 YEAR\",\"min\":100000,\"max\":165000}]"
}
```

Real baseline examples and the live verification record are in `examples/`. The synthetic demonstration is labeled separately.

### Coverage and limitations

- Only the documented [Ashby public posting API](https://developers.ashbyhq.com/docs/public-job-posting-api). No credentials, applicant data, private or explicitly unlisted jobs.
- Employers may publish salaries only in prose. This version does not extract numbers from descriptions, infer missing pay or promise universal salary coverage.
- Only base salary components are compared. Equity, bonuses, benefits and job-description changes are outside this version.
- Maximum 20 boards per run, 5,000 jobs and 16 MiB response per board. Oversized or malformed responses fail the board instead of saving a truncated snapshot.
- Two successful absences are required before `REMOVED` by default. A failed request neither advances absence counts nor overwrites history. A repeated valid but incomplete source response can still look like a removal.
- `REMOVED` means no longer listed, not necessarily hired, expired or cancelled. No historical data exists before your first run. Removed listings are retained for 90 days; later reappearances are treated as new.
- Run one schedule per watch list without overlap. A concurrent-write check catches some collisions, but there is no distributed atomic lock. Do not share a watch list across parallel runs.
- Output and state writes are not a single transaction. A storage failure can cause a later retry to repeat an observation. Exactly-once delivery is not promised.
- A partial source failure retains successful-board results, writes `SUMMARY`, and marks the run failed so failure integrations can detect it.

### Pricing

This beta has **no developer fee**: no charge per board or output row. Apify bills platform usage according to your plan; available platform credits can cover that usage. Free beta does not mean unlimited free compute or storage.

A cloud test of one board with 71 baseline rows used approximately $0.00058 of platform usage; an immediate repeat used approximately $0.00026. These are individual measurements, not a price guarantee, and exclude builds and subsequent storage. Larger boards, retries and frequent schedules cost more.

Start with one board and a daily schedule. Review your Apify usage and retain only the output you need. Any future paid pricing will be shown in the Store under Apify's pricing-change rules.

### Local development

Node 22 or newer:

```bash
npm ci --ignore-scripts
npm test
npm run demo
npm run smoke
npm run integration
```

`smoke` makes two public requests each to Ashby, Linear and Notion, saves aggregate measurements and one baseline sample per board. No accounts or paid services are used.

To run the Apify SDK locally, save the JSON input above to `storage/key_value_stores/default/INPUT.json`, then:

```bash
APIFY_PURGE_ON_START=false npm start
```

Repeat with the same input to observe deltas. Local state lives in `storage/key_value_stores/ashby-pay-<monitorId>`; deleting only that directory resets this watch list. `storage/` is ignored by Git and Docker.

Deployment and launch tracking: [docs/launch.md](docs/launch.md). Research and economics: [docs/research.md](docs/research.md).

# Actor input Schema

## `boards` (type: `array`):

1–20 board names or https://jobs.ashbyhq.com/<company> URLs. Free beta: no developer fee; standard Apify platform usage applies.

## `monitorId` (type: `string`):

Reuse this name for subsequent runs. Use a different name for a fresh history. Do not overlap runs with the same name.

## `outputMode` (type: `string`):

Changes returns baseline rows on the first run, then only differences. Snapshot also includes unchanged jobs. Both update history.

## `emitInitial` (type: `boolean`):

In changes mode, include BASELINE rows on the first check. They are existing listings, not newly published jobs.

## `removalConfirmations` (type: `integer`):

A job must be absent in this many successful checks. Failed requests never count. Removal does not mean the role was filled.

## Actor input object example

```json
{
  "boards": [
    "Ashby"
  ],
  "monitorId": "default",
  "outputMode": "snapshot",
  "emitInitial": true,
  "removalConfirmations": 2
}
```

# Actor output Schema

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

Baseline and observed changes to public job listings and salary ranges.

# 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 = {
    "boards": [
        "Ashby"
    ],
    "outputMode": "snapshot"
};

// Run the Actor and wait for it to finish
const run = await client.actor("verdant_pine/ashby-compensation-monitor").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 = {
    "boards": ["Ashby"],
    "outputMode": "snapshot",
}

# Run the Actor and wait for it to finish
run = client.actor("verdant_pine/ashby-compensation-monitor").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 '{
  "boards": [
    "Ashby"
  ],
  "outputMode": "snapshot"
}' |
apify call verdant_pine/ashby-compensation-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,verdant_pine/ashby-compensation-monitor"
        }
    }
}

```

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/ukCxKTSBHK7xDvLTp/builds/vKEkI5IDZIIgPWSxf/openapi.json
