# How We Turn a 35% BLS PMHNP Growth Projection Into Search, Alerts, and Better Job Matches

The BLS projection (35% PMHNP growth from 2024–2034) is a macro signal. The hard part is translating it into a daily system that answers: where are the roles, what do they pay, how fast do they move, and which postings are actually real.

# How We Turn a 35% BLS PMHNP Growth Projection Into Search, Alerts, and Better Job Matches

The BLS projection of **35% PMHNP job growth (2024–2034)** is a clean number that shows up in headlines. As builders, we treat it as a *macro input*—useful, but not directly actionable.

What’s actionable is what that trend turns into at the job-posting layer:

- more postings (and more duplicates)
- faster hiring cycles (time-to-fill compresses)
- wider variance by state, setting, and employer type
- more compensation noise (ranges, bonuses, productivity, telehealth modifiers)

At **PMHNP Hiring**, we aggregate from **500+ job sources daily** and maintain **10,000+ verified PMHNP jobs across 50 states**. The goal isn’t to repeat the BLS number—it’s to make it queryable: *where is growth showing up today, and what does “good role” mean in that market?*

## Macro projection → micro signals we can measure

The 35% projection is best read as sustained demand: more people seeking care, expanding access efforts, and pressure on psychiatry capacity. PMHNPs sit right in the middle.

But “more jobs” doesn’t mean “every job is a fit.” So we track operational signals that correlate with a heated market:

- **posting velocity** (new jobs per day/week by state + setting)
- **time-to-fill proxies** (how long a posting stays live, how often it gets refreshed)
- **credentialing friction** (signals in text like “credentialing support,” “start in 2–4 weeks,” etc.)
- **comp package completeness** (whether salary, schedule, supervision, and ramp are specified)

One example we surface internally is a rolling *days-live* metric. It’s not perfect, but it’s observable at scale.

```sql
-- Rough “days live” metric from our normalized postings table
select
  state,
  percentile_cont(0.5) within group (order by (now() - first_seen_at)) as median_days_live,
  count(*) as active_jobs
from jobs
where
  role = 'PMHNP'
  and status = 'active'
group by state
order by median_days_live asc;
```

If median days-live drops, it often matches what clinicians feel as “employers are moving faster.” In the original blog, we cited time-to-fill tightening (e.g., ~32 days vs ~45). We treat those as hypotheses and validate them against our own observed posting lifecycle.

## The ingestion pipeline: 500+ sources, one schema

Scraping PMHNP jobs isn’t “fetch HTML, parse title.” Every source has its own quirks:

- different location formats (city/state, remote, multi-state, “within 50 miles”)
- different salary formats (hourly, annual, per-visit, wide ranges)
- duplicated listings across ATS platforms, job boards, and staffing agencies
- stale posts that get re-published with new IDs

Our pipeline is built to turn that mess into a stable contract:

1. **Fetch** (scheduled jobs) from boards/ATS feeds
2. **Parse** into a common intermediate model
3. **Normalize** fields (title → role, location → geo, salary → annualized range)
4. **Deduplicate** and **verify**
5. **Index** for real-time filtering + alerts

Tech stack pieces:

- **Next.js + TypeScript** for UI and API routes
- **Supabase (Postgres)** for storage + full-text search + RLS
- **Stripe** for billing (alerts/subscriptions)

## Deduplication: the “more jobs” trap

Job growth increases volume, but it also increases duplicates. A single PMHNP opening can appear on:

- the employer site
- an ATS mirror
- 2–5 job boards
- a staffing listing with rewritten text

If we don’t dedupe, users think there are more unique opportunities than there are—and alerts become spam.

We generate a fingerprint using a mix of deterministic and fuzzy signals:

- normalized employer name
- canonicalized location (lat/lng + radius buckets)
- role taxonomy (PMHNP vs “Psych NP” variants)
- compensation overlap (when present)
- text similarity on responsibilities and requirements

Pseudo-code sketch:

```ts
type Job = {
  title: string
  employer: string
  city?: string
  state?: string
  lat?: number
  lng?: number
  description: string
  salaryMin?: number
  salaryMax?: number
}

function fingerprint(job: Job) {
  return hash([
    normalizeEmployer(job.employer),
    normalizeRole(job.title),
    geoBucket(job.lat, job.lng, 0.25),
    salaryBucket(job.salaryMin, job.salaryMax),
    simhash(normalizeText(job.description))
  ].join('|'))
}
```

This is what turns macro growth into a trustworthy count of *verified* jobs.

## Salary data: why “$139K–$155K” is a normalization problem

Clinically, people want to know pay. Technically, pay is one of the messiest fields we ingest.

Common failure modes:

- hourly rates without hours/week
- “$120k–$250k” ranges that include productivity/bonus but aren’t labeled
- sign-on bonuses mixed into base
- “per diem” roles listed as annual
- DNP vs MSN differentials inconsistently stated

So we normalize salaries into an annualized range with metadata:

- `pay_type` (hourly/annual/unknown)
- `annual_min`, `annual_max`
- `confidence_score`
- `includes_bonus` (best-effort)

Example normalization logic:

```ts
function annualize({ payType, min, max, hoursPerWeek = 40 }: any) {
  if (payType === 'hourly') {
    return {
      annualMin: min * hoursPerWeek * 52,
      annualMax: max * hoursPerWeek * 52,
    }
  }
  return { annualMin: min, annualMax: max }
}
```

That’s how we can talk about national ranges (like ~$139K–$155K common bands, entry-level around ~$126K) while still being honest about variance and data quality.

## What the growth signal means for “career confidence” (as data)

The BLS projection improves the odds that you’ll find opportunities—but the *quality* of those opportunities depends on factors you can filter for:

- setting (outpatient, community mental health, integrated care, telepsych)
- onboarding support (mentorship/supervision signals)
- realistic ramp + admin time
- credentialing speed

From a product standpoint, this is why we invest in structured fields extracted from unstructured text. A fast offer can correlate with staffing pressure; we try to surface the context so users can choose strategically.

In other words: the 35% growth projection is the headline. The system work is turning it into a search experience where you can reliably answer, **“Where are the real roles, and which ones are built to support me once I’m hired?”**
