# How We Compared Telehealth vs In-Person PMHNP Pay Across 10,000+ Job Posts

“Telehealth pays less” is usually a conclusion drawn from one offer. When you aggregate thousands of postings and normalize comp structures, the pattern flips: telehealth often pays more.

## The myth: telehealth is “easier,” so it pays less

On PMHNP Hiring we ingest 500+ sources daily and maintain 10,000+ verified PMHNP jobs across all 50 states. When we look at compensation across that dataset (after normalizing salary formats and removing duplicates), the common claim that *in-person always pays more* doesn’t hold up.

Across postings that include usable pay data, **telehealth roles often price higher** than in-person roles.

That doesn’t mean every remote job beats every onsite job. It means the distribution is different enough that treating telehealth as a “pay cut for flexibility” is a bad default.

This post is the builder’s version of the question: what does the data say, and what did we have to do technically to make it comparable?

---

## Why pay comparisons are hard (and why raw job boards mislead)

Job posts rarely ship “clean” salary fields. The same compensation can show up as:

- `$140/hr` (W2 hourly)
- `$1,200/day`
- `$250/visit` (1099)
- `Up to $220k` (base + bonus unknown)
- `80% collections` (requires assumptions)

If you compare those strings directly, you’ll produce nonsense. Our pipeline has to:

1. **Extract** comp from messy text (structured fields when available, otherwise description parsing)
2. **Normalize** to comparable units (hourly ↔ annual, ranges ↔ midpoint)
3. **Classify** pay model (salary, hourly, per-visit, RVU/collections)
4. **Deduplicate** cross-posted roles so one high-paying listing doesn’t appear 30 times
5. **Segment** by modality (telehealth vs in-person vs hybrid) using both metadata and text signals

Only after that do “telehealth vs in-person” comparisons become meaningful.

---

## The pipeline: from scraped postings to comparable numbers

At a high level, we treat each source as an input adapter that maps into a common schema, then run enrichment steps.

### 1) Canonical job schema

We store a normalized representation (Supabase/Postgres), keeping raw fields for debugging:

```ts
type PayModel = 'salary' | 'hourly' | 'per_visit' | 'rvu' | 'collections' | 'unknown'

type Job = {
  id: string
  source: string
  source_job_id: string
  title: string
  company: string
  location_text: string
  remote_type: 'telehealth' | 'in_person' | 'hybrid' | 'unknown'
  pay_model: PayModel
  pay_min?: number
  pay_max?: number
  pay_unit?: 'year' | 'hour' | 'visit'
  pay_currency?: 'USD'
  description: string
  posted_at: string
  fingerprint: string // for dedupe
}
```

### 2) Salary parsing + normalization

We normalize into an annualized estimate **only when the pay model supports it**. For hourly W2 roles, annualization is straightforward (with assumptions). For per-visit/collections, we keep the model explicit to avoid inventing certainty.

```ts
const HOURS_PER_YEAR = 2080

function annualize(job: Job) {
  if (job.pay_model === 'hourly' && job.pay_unit === 'hour') {
    return {
      annual_min: job.pay_min ? job.pay_min * HOURS_PER_YEAR : null,
      annual_max: job.pay_max ? job.pay_max * HOURS_PER_YEAR : null,
      confidence: 'medium',
    }
  }

  if (job.pay_model === 'salary' && job.pay_unit === 'year') {
    return {
      annual_min: job.pay_min ?? null,
      annual_max: job.pay_max ?? null,
      confidence: 'high',
    }
  }

  // per-visit / collections / RVU require volume assumptions → do not annualize by default
  return { annual_min: null, annual_max: null, confidence: 'low' }
}
```

This is where a lot of “telehealth pays less” myths come from: many remote roles are posted as per-visit or production-based, while hospital roles are posted as clean annual salaries. If you only compare annual-salary postings, you bias toward in-person systems.

### 3) Deduplication (the hidden salary inflation bug)

High-volume telehealth platforms syndicate aggressively. Without dedupe, your dataset overcounts the same role and skews pay stats.

We generate a fingerprint from stable fields (company + title + state/license requirement + pay band + remote type) and cluster near-matches.

Architecture note: dedupe is a blend of deterministic hashing + fuzzy matching (string similarity on company/title) with thresholds tuned by manual review.

---

## What the data shows: telehealth often prices higher

After normalization and dedupe, we compare distributions by `remote_type`, segmented by pay model (salary vs hourly vs per-visit).

A simplified SQL sketch:

```sql
select
  remote_type,
  pay_model,
  percentile_cont(0.5) within group (order by annual_mid) as p50,
  count(*) as n
from (
  select
    remote_type,
    pay_model,
    case
      when annual_min is not null and annual_max is not null then (annual_min + annual_max)/2
      when annual_min is not null then annual_min
      when annual_max is not null then annual_max
      else null
    end as annual_mid
  from job_comp_normalized
  where annual_min is not null or annual_max is not null
) x
group by 1,2
order by n desc;
```

The repeated pattern we see:

- **Telehealth salary/hourly postings often cluster higher** than comparable in-person postings.
- The gap gets bigger in roles that signal urgency: multi-state licensing, nights/weekends, fast start dates.
- In-person still wins in specific slices: hospital systems with strong benefits and stable base salaries.

So why does telehealth price higher *so often*?

---

## The business math behind the pay (as seen through job post signals)

From a data standpoint, remote roles correlate with signals that predict higher comp:

1. **Competition is national, not local**
   - Telehealth employers compete against other remote-first orgs. We see faster repost cycles and higher pay edits in these listings.

2. **Many remote models are throughput-optimized**
   - Posts mention standardized workflows, shorter appointment gaps, and reduced no-shows. That tends to pair with productivity pay or higher hourly rates.

3. **Coverage + urgency premiums**
   - Remote roles disproportionately include nights/weekends, rural coverage, and “licensed in X state” requirements.

Technically, these show up as text features we can index and filter: `weekend`, `after-hours`, `multi-state`, `compact`, `ASAP`, etc. They’re not perfect, but they’re strong enough to segment on.

---

## What we surface in the product (and why it matters for negotiation)

On the UI side (Next.js + TypeScript), we expose filters that map directly to the normalized schema:

- Telehealth / in-person / hybrid
- Pay model (salary vs hourly vs per-visit)
- Pay range (only when confidence is sufficient)
- State licensing requirements

Alerts (email/push) are triggered when new postings match saved filters, so users can watch *their* slice of the market rather than relying on anecdotes.

If you’re negotiating, the practical takeaway is data-driven: **don’t assume telehealth implies a discount**. Treat modality as one variable, then compare roles with the same pay model and similar constraints.

---

## Next up: improving apples-to-apples comparisons

The hardest remaining problem is per-visit and collections-based comp. We’re working on “expected annual comp” estimates by pairing postings with realistic volume assumptions (and clearly labeling them as assumptions). That’s the only way to compare a $250/visit role against a $190k base role without hand-waving.
