# Job Pay-Transparency Extractor (`steadfeed/pay-transparency-extractor`) Actor

Extract pay ranges from public Greenhouse, Lever and Ashby job feeds, with evidence and company disclosure summaries. Flag postings without an extracted range. Informational extraction, not legal compliance advice. By Steadfeed.

- **URL**: https://apify.com/steadfeed/pay-transparency-extractor.md
- **Developed by:** [Steadfeed](https://apify.com/steadfeed) (community)
- **Categories:** Jobs
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job successfully checkeds

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

## Job Pay-Transparency Extractor — Steadfeed

**Version 0.2.0 — job-level evidence, company summaries and guarded pay-per-event billing.**

Extract salary/pay ranges from public **Greenhouse**, **Lever (US and EU)** and **Ashby** job-board feeds. Get job-level evidence and a summary of the share of checked postings with an extracted pay range. Useful for research and data quality review—not legal determinations.

> **Informational data extraction, NOT legal compliance advice. A missing range in a posting does not by itself mean a law was broken.** This Actor does not determine whether a law applies, whether an employer has complied, or whether a posting is lawful. Location filters do not encode legal requirements. A `false` disclosure flag means the extractor did not obtain a two-bound range from the fetched sources, not that no range exists anywhere.

### Input example

```json
{
  "boards": [
    {"ats": "greenhouse", "slug": "discord", "company": "Discord"},
    {"ats": "lever", "slug": "palantir", "company": "Palantir", "region": "us"},
    {"ats": "ashby", "slug": "ramp", "company": "Ramp"}
  ],
  "maxJobsPerCompany": 10,
  "locations": [],
  "countries": [],
  "remoteOnly": false,
  "titleKeywords": []
}
```

Supply **board slugs, not arbitrary URLs**. A display name is optional; otherwise the source company name or board slug is used. Lever EU uses `"region":"eu"`. Other ATS types do not accept a region.

#### Strict validation

Invalid input fails before any network request or dataset output: unknown keys (including nested board keys), missing/empty boards, unsupported ATS, unsafe slugs, duplicate boards, invalid region/country codes, wrongly typed booleans, invalid caps and empty/oversized filter strings. There is no silent type coercion. Boards: 1–20. Cap: integer 1–1000 per board, default 100. Filter arrays: up to 30 entries, each 1–100 characters. Runtime also validates every output record before writing results.

#### Filters and scope

- `locations`: case-insensitive literal words/phrases in listed locations. Examples: `California`, `New York`, `Colorado`. Short abbreviations can be ambiguous; full state names are preferable.
- `countries`: uppercase ISO country codes (`US`, `GB`, `DE`), or `EU` expanded to 27 EU members. Best-effort inference from source country fields, explicit country names/aliases, or US state names/codes. City-only postings can be excluded. This is not geocoding or jurisdiction analysis.
- `remoteOnly`: explicitly remote jobs only. Hybrid and unknown are excluded. Missing workplace information stays unknown (`null`), not automatically onsite.
- `titleKeywords`: case-insensitive literal substrings, such as `engineer` or `designer`; not regular expressions.
- OR within a filter list, AND between groups. Filters are applied before the check cap; cap follows feed order, not random sampling.
- Company percentages cover **only jobs checked after filters and caps**, not necessarily the company's full workforce or board. `partial` flags caps, feed truncation or unavailable details affecting results. `jobsAvailable` and `jobsMatched` make this scope visible.

### Extraction

1. Fetch official public board feeds without authentication or proxies. Lever pagination is supported, capped at 20,000 listed jobs per board for safety.
2. Request Greenhouse public job detail with `pay_transparency=true` for selected jobs. Read Lever `salaryRange` and Ashby compensation tiers/components.
3. Prefer structured salary/wage fields. Ashby equity and bonus components are not treated as salary. A source-labelled Salary component can itself be OTE; `compensationBasis` marks explicit OTE/base wording where present, and raw evidence must be reviewed. Unspecified does not mean base salary. If structured numeric pay is unavailable, scan description text for explicit numeric money ranges.
4. Preserve all distinct structured geographic/level tiers in `payRanges`. Do **not** combine them into an artificial minimum-to-maximum envelope. When there are multiple ranges, flat min/max/currency/period fields are `null`; inspect the array.
5. No currency conversion or annualization. Greenhouse cents are divided by 100 as specified by its source fields. Unknown periods/currencies remain `null`; bare `$` does not imply USD. Hour/year/month/week/day are supported when stated. Greenhouse frequently does not state a structured interval, so it remains unknown.

#### Evidence and interpretation

- `payRangeDisclosed`: true when at least one extracted range has both valid numeric bounds, including explicitly equal bounds.
- `payFound`: true if at least one structured/parsed numeric range or single structured bound was obtained.
- `extractionStatus`: `parsed_range`, `single_value`, `missing`, `unparseable`, or `source_error`.
- `missing` means no recognized numeric range/pay evidence was found. `unparseable` is a conservative review flag for pay-related text with numbers that could not be extracted; it can include irrelevant numbers/benefits. Neither status is a legal conclusion.
- Single structured values/open-ended ranges do not count as a disclosed two-bound range. Single-value description formats are not parsed in this version.
- `paySource`: `structured`, `description`, or `null`. Structured evidence is the raw field JSON or source-provided component summary; description evidence is the nearby raw text snippet.
- `structuredCheckComplete`: false if Greenhouse detail could not be fetched. A description range can still be reported, but a job with failed detail and no extracted pay is a `source_error`, not counted as undisclosed.

Structured fields take precedence; description alternatives are not used to override structured pay. This Actor does not reconcile contradictions between source fields and page text.

### Output

Default dataset contains `recordType: "job"` records and one `recordType: "company_summary"` per input board. Select by `recordType` in your downstream tools. Three Console views project overview, evidence and company-summary columns; they **do not filter out other record types**. `COMPANY_SUMMARIES` in the default key-value store is the clean summaries-only array. `SUMMARY` contains run totals and filters.

Job records include company/slug, title, job ID, locations, inferred countries, remote flag and workplace type, ATS/region, HTTPS job URL, source posted/updated dates, check timestamp, flat pay min/max/currency/period, raw pay text, evidence source, `payRanges`, disclosure flag and extraction status. Source dates remain `null` if not supplied; they are not replaced with the check date. Dates in machine-readable output use ISO 8601.

Company records include available/matched/checked job counts, budget stop reason, disclosed count, no-range-found count (including unparseable/single-value postings), unparseable count, source-error count, detail-lookup-failure count, disclosure percentage, status and partial flag. Percentage = disclosed / successfully checked × 100, rounded to two decimals; null when none were checked. Board fetch errors produce an error summary, not invented empty/undisclosed jobs. Budget-exhausted boards are skipped without network requests and get free skipped_limit summaries with unknown (null) available/matched counts. A partially checked board gets limit_reached and a charge_limit stop reason. Greenhouse detail failures without pay evidence are excluded from the percentage denominator.

Formal schemas live in `.actor/record_schema.json`, `.actor/dataset_schema.json`, `.actor/output_schema.json`, and `.actor/input_schema.json`.

### Limits and responsible use

- Public ATS data only, three providers in this version. Workable, SmartRecruiters, Workday, PDFs, screenshots and custom career pages are not supported.
- Text parsing is heuristic, primarily English compensation wording with common currencies and numeric range formats. Unusual formats, languages, open-ended amounts, tax/net distinctions, OTE/total compensation phrasing and hidden page elements may be missed or misclassified. Review evidence before relying on it.
- A feed is not guaranteed to match every rendered job page. Ranges may vary by location, level or employment type. Retain the raw evidence and source URL for review.
- Best-effort country inference can be ambiguous (e.g. state/country names); use explicit location filters and spot-check. It is not a legal location resolver.
- Source sites may block or rate-limit requests, move feeds or change schemas. Timeouts, per-host spacing and bounded retries are included. Errors are visible in summaries. No captcha bypass, login scraping or proxy evasion.
- Each output record is schema-validated before writing; job results are streamed as checks complete. At most 20 boards × 1000 selected checks; large full-feed responses and the collected run result can still use memory. Use smaller caps for testing. Source order means capped results are not representative samples.
- No customer messages, auto-applications, legal claims, secrets, private job data or credential fields are requested. Do not put credentials in input. Pricing changes are account settings, never performed by Actor code. The code emits only the explicit job-checked event when PPE is configured.

### Local development

Requires Node.js 22+.

```sh
npm ci
npm test
apify validate-schema
npm run test:live
## Optional local Actor SDK execution (local storage only):
apify run --input-file inputs/smoke.json
```

Live test snapshots, results and manual spot-check notes are kept locally under `test-results/` and excluded from deployment. Unit tests do not call live APIs.

### Pay-per-event billing and budget limits

When PPE is configured, **$0.002 per successfully checked job** is charged through `job-checked`, once per complete check, **whether or not a pay range was found**. Filtered-out jobs are not checked or charged. Company summaries, source-error records and incomplete source fetches (including a Greenhouse detail failure with a usable description fallback) do not incur this job event.

The platform's separate **Actor Start** event is $0.00005 per GB of allocated memory, minimum one event, matching Steadfeed's Auditor. It can apply even when there are no checked jobs or the run fails. No automatic per-dataset-item event is permitted; the Actor refuses to run under PPE if that synthetic event is present or the configured job/start rates differ from these documented rates. Ordinary platform usage costs follow the effective Apify pricing configuration; owner development runs can still incur resource costs.

The Actor checks the remaining `job-checked` capacity **before fetching another board**, **before checking another job**, and again immediately before charging. It stops checks cleanly when no event fits the user's maximum total charge, logs the reason, saves partial results, and emits the remaining company summaries without job charges. The run's `SUMMARY.billing` shows charged-job count, uncharged job rows and whether the limit was reached. Start charges reduce the available event budget as tracked by Apify's charging manager.

Rows are validated and written before the explicit `Actor.charge({eventName: "job-checked", count: 1})` call, so a failed output write does not incur a new job charge. If the charge API fails after a row was saved, the run fails visibly rather than retrying or misreporting it as a source error; that last saved row may be uncharged. Rerunning a job is a new check and may be charged again. Unpriced private development runs do not invoke product-event charging.

### Deployment safeguards

Visibility and pricing are managed through the official Apify API/Console, not by this Actor at runtime. Publication and live pricing changes require explicit owner approval. Verify the intended account and private build results before release, and verify the effective PPE record and public listing afterward. This project needs no Git remote or PR.

### Credits and version

Version 0.2.0. Public-feed endpoint patterns, text/ISO/workplace helpers and polite HTTP handling adapted from the existing Steadfeed ATS Job Board Monitor. This standalone Actor does not change that Actor or share its monitor state.

# Changelog

This Actor's version history is a separate document: https://apify.com/steadfeed/pay-transparency-extractor/changelog.md

# Actor input Schema

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

1–20 objects: ats (greenhouse, lever, ashby), slug (board slug, not URL), optional company display name, optional Lever region us/eu. Invalid or duplicate boards are rejected before fetching.

## `locations` (type: `array`):

Optional literal words or phrases in listed locations, e.g. California, New York. No law/jurisdiction determinations. OR within the list, AND with other filter groups.

## `countries` (type: `array`):

Uppercase ISO country codes, e.g. US, GB, DE, or EU for the 27 EU members. Best-effort matching of explicit country/state location evidence; city-only or ambiguous locations may be excluded.

## `remoteOnly` (type: `boolean`):

Include only explicitly remote jobs; excludes hybrid and unknown.

## `titleKeywords` (type: `array`):

Case-insensitive literal substring matching; not regular expressions.

## `maxJobsPerCompany` (type: `integer`):

1–1000. Jobs are checked in source order after filters. Summaries mark capped samples as partial.

## Actor input object example

```json
{
  "boards": [
    {
      "ats": "greenhouse",
      "slug": "discord",
      "company": "Discord"
    }
  ],
  "remoteOnly": false,
  "maxJobsPerCompany": 100
}
```

# Actor output Schema

## `jobsAndSummaries` (type: `string`):

No description

## `evidence` (type: `string`):

No description

## `companies` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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": [
        {
            "ats": "greenhouse",
            "slug": "discord",
            "company": "Discord"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfeed/pay-transparency-extractor").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": [{
            "ats": "greenhouse",
            "slug": "discord",
            "company": "Discord",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("steadfeed/pay-transparency-extractor").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": [
    {
      "ats": "greenhouse",
      "slug": "discord",
      "company": "Discord"
    }
  ]
}' |
apify call steadfeed/pay-transparency-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfeed/pay-transparency-extractor"
        }
    }
}
```

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/1RYPBgOxC8bSy0nlx/builds/ECxPMXyVAzREiKSce/openapi.json
