{
  "report": "ghost-job-signals",
  "quarter": "2026-Q3",
  "snapshotDate": "2026-09-19",
  "snapshotAt": "2026-09-19T18:26:39.327Z",
  "license": "CC BY 4.0",
  "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
  "methodologyUrl": "https://four-leaf.ai/research/methodology",
  "reportUrl": "https://four-leaf.ai/research/ghost-job-signals-2026-q3",
  "datasetUrl": "https://four-leaf.ai/research/ghost-job-signals-2026-q3.csv",
  "source": "Four-Leaf live job index, postings pulled directly from company career pages",
  "generatedBy": "scripts/research/ghost-job-signals-2026-q3.ts",
  "findings": [
    "The index held 149,008 active postings from 1,319 companies on 2026-09-19.",
    "26,249 active postings, 17.6%, had been live for more than 60 days.",
    "12,179 active postings, 8.2%, had been live for more than 120 days.",
    "1,360 distinct company and title pairs appeared 5 or more times in the index, and 308 appeared 12 or more times.",
    "20,467 active postings belonged to a role repeated 5 or more times at the same company.",
    "44,482 active postings, 29.9%, carried at least one structural ghost-job signal.",
    "The median active posting had been live 17 days, and the slowest tenth had been live at least 101 days."
  ],
  "population": {
    "totalActivePostings": 149008,
    "companies": 1319,
    "postingsWithUnknownPostedDate": 0,
    "postingsWithImplausiblePostedDate": 479,
    "postingsDatedAfterSnapshot": 0,
    "postingsDroppedForNullCompanyOrTitle": 0
  },
  "signals": {
    "liveOver60Days": {
      "count": 26249,
      "pct": 17.6
    },
    "liveOver120Days": {
      "count": 12179,
      "pct": 8.2
    },
    "distinctRolesRepeated5Plus": 1360,
    "distinctRolesRepeated12Plus": 308,
    "postingsInRolesRepeated5Plus": 20467,
    "anyStructuralSignal": {
      "count": 44482,
      "pct": 29.9
    },
    "overlap": {
      "ageSignalOnly": {
        "count": 24015,
        "pct": 16.1
      },
      "repeatSignalOnly": {
        "count": 18233,
        "pct": 12.2
      },
      "bothSignals": {
        "count": 2234,
        "pct": 1.5
      }
    }
  },
  "distributions": {
    "basis": "148,529 active postings with a believable posted_at",
    "medianDaysLive": 17,
    "p90DaysLive": 101,
    "daysLive": [
      {
        "label": "0 to 7 days",
        "min": 0,
        "max": 7,
        "count": 40880,
        "pctOfDatedPostings": 27.5
      },
      {
        "label": "8 to 30 days",
        "min": 8,
        "max": 30,
        "count": 57637,
        "pctOfDatedPostings": 38.8
      },
      {
        "label": "31 to 60 days",
        "min": 31,
        "max": 60,
        "count": 23763,
        "pctOfDatedPostings": 16
      },
      {
        "label": "61 to 120 days",
        "min": 61,
        "max": 120,
        "count": 14070,
        "pctOfDatedPostings": 9.5
      },
      {
        "label": "121 to 180 days",
        "min": 121,
        "max": 180,
        "count": 4694,
        "pctOfDatedPostings": 3.2
      },
      {
        "label": "Over 180 days",
        "min": 181,
        "max": null,
        "count": 7485,
        "pctOfDatedPostings": 5
      }
    ],
    "repeatCount": [
      {
        "label": "Listed once",
        "min": 1,
        "max": 1,
        "count": 106563,
        "pctOfActivePostings": 71.5
      },
      {
        "label": "Listed 2 to 4 times",
        "min": 2,
        "max": 4,
        "count": 21978,
        "pctOfActivePostings": 14.7
      },
      {
        "label": "Listed 5 to 11 times",
        "min": 5,
        "max": 11,
        "count": 6866,
        "pctOfActivePostings": 4.6
      },
      {
        "label": "Listed 12 or more times",
        "min": 12,
        "max": null,
        "count": 13601,
        "pctOfActivePostings": 9.1
      }
    ]
  },
  "thresholds": {
    "staleDays": 60,
    "veryStaleDays": 120,
    "repeatThreshold": 5,
    "heavyRepeatThreshold": 12,
    "epochFloor": "2000-01-01T00:00:00Z"
  },
  "definitions": {
    "activePosting": "A row in the index flagged is_active, meaning the posting was still listed at the company source on the snapshot date.",
    "role": "A distinct company plus normalized job title, where normalizing means collapsing every run of whitespace to a single space, trimming, and lowercasing. Two postings whose titles differ only by spacing are the same role. The CSV publishes this value as title_norm so the grouping reproduces exactly.",
    "daysLive": "Whole days between posted_at and the snapshot instant, rounded down. Empty when the posting's age is unknown. The age signals are defined on this integer, so filtering the CSV on days_live reproduces the published counts exactly.",
    "structuralGhostJobSignal": "A posting live for more than 60 days, OR one belonging to a role repeated 5 or more times at the same company. Computed over every active posting from raw columns. It is a prompt to verify a listing, not a finding that the listing is fake.",
    "ageUnknown": "A posting with no posted_at, or one whose posted_at falls below the epoch floor of 2000-01-01, cannot satisfy either age test. Such postings count toward the denominator and never toward the 60 or 120 day figures. In the CSV they are the rows with an empty days_live, and posted_at_implausible separates the two reasons.",
    "implausiblePostedDate": "One ATS feed returns a misparsed integer as posted_at, stamping those postings January 1970. Any posted_at below 2000-01-01 is treated as unknown rather than as a posting live for decades. The index holds no posting dated between 1970 and 2009, so the floor's exact placement inside that gap changes no figure. Postings genuinely dated 2009 onward are kept and do count toward the age tests."
  },
  "notPublishedHere": {
    "contentSignals": "The ghost job checker also reads the language of an individual ad. That analysis runs per lookup and has never been run across the index, so no index-wide share of it exists and none is reported here.",
    "applyLinkLiveness": "Apply-link liveness is checked per lookup for signed-in users, not across the index, so it is absent from these figures.",
    "storedLegitimacyTier": "scraped_jobs.legitimacy_tier is a cached column refreshed in capped daily batches, so at any moment it is a mix of tiers stamped weeks apart rather than a snapshot. It is deliberately not used here."
  },
  "methodChanges": {
    "since": "2026-09-16",
    "note": "The 2026-09-16 snapshot applied no floor to posted_at, so postings carrying a misparsed epoch counted as stale. This run treats 479 such postings as unknown-age instead, which lowers the 60 and 120 day counts by that amount at most. Figures also move because is_active is overwritten in place and no prior snapshot is recoverable."
  },
  "reproduce": {
    "note": "The CSV recomputes every figure above from its own columns. Group on company plus title_norm rather than company plus title, because the repeat counts are computed on the normalized title. The SQL below returns the headline aggregates, not the distributions or the overlap counts, and it runs against the index as it stands rather than against this snapshot: is_active is overwritten in place and the table keeps no last-seen timestamp, so a past snapshot cannot be rebuilt. Run scripts/research/ghost-job-signals-2026-q3.ts to take a fresh one.",
    "sql": "with active as (\n  select company,\n    lower(btrim(regexp_replace(title, '[\\s\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+', ' ', 'g'))) as title_norm,\n    posted_at\n  from scraped_jobs\n  where is_active and company is not null and title is not null\n), dupes as (\n  select company, title_norm, count(*) as n from active group by 1, 2\n), j as (\n  select a.*, d.n,\n    (a.posted_at is not null and a.posted_at >= timestamptz '2000-01-01T00:00:00Z') as age_known,\n    floor(extract(epoch from (now() - a.posted_at)) / 86400) as days_live\n  from active a join dupes d using (company, title_norm)\n), k as (\n  select j.*,\n    (age_known and days_live > 60)  as stale_60,\n    (age_known and days_live > 120) as stale_120,\n    (n >= 5) as reposted_5plus\n  from j\n)\nselect\n  count(*) as total_active_postings,\n  count(distinct company) as companies,\n  count(*) filter (where not age_known) as age_unknown,\n  count(*) filter (where stale_60) as live_over_60d,\n  round(100.0 * count(*) filter (where stale_60) / count(*), 1) as live_over_60d_pct,\n  count(*) filter (where stale_120) as live_over_120d,\n  round(100.0 * count(*) filter (where stale_120) / count(*), 1) as live_over_120d_pct,\n  (select count(*) from dupes where n >= 5) as roles_repeated_5plus,\n  (select count(*) from dupes where n >= 12) as roles_repeated_12plus,\n  count(*) filter (where reposted_5plus) as postings_in_repeated_roles,\n  count(*) filter (where stale_60 or reposted_5plus) as any_signal,\n  round(100.0 * count(*) filter (where stale_60 or reposted_5plus) / count(*), 1) as any_signal_pct\nfrom k;"
  }
}
