{
  "report": "ghost-job-signals",
  "quarter": "2026-Q4",
  "snapshotDate": "2026-10-21",
  "snapshotAt": "2026-10-21T20:59:12.641Z",
  "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-q4",
  "datasetUrl": "https://four-leaf.ai/research/ghost-job-signals-2026-q4.csv",
  "source": "Four-Leaf live job index, postings pulled directly from company career pages",
  "generatedBy": "scripts/research/ghost-job-signals-2026-q4.ts",
  "findings": [
    "The index held 149,201 active postings from 1,343 companies on 2026-10-21.",
    "27,191 active postings, 18.2%, had been live for more than 60 days.",
    "11,853 active postings, 7.9%, had been live for more than 120 days.",
    "1,325 distinct company and title pairs appeared 5 or more times in the index, and 313 appeared 12 or more times.",
    "20,775 active postings belonged to a role repeated 5 or more times at the same company.",
    "45,805 active postings, 30.7%, carried at least one structural ghost-job signal.",
    "The median active posting had been live 19 days, and the slowest tenth had been live at least 100 days.",
    "7,651 active postings, 5.1% of the 148,806 with a believable posted date, had been live for more than 180 days."
  ],
  "comparisonFindings": [
    "30.7% of active postings carried at least one structural ghost-job signal on 2026-10-21 (45,805 of 149,201), against 29.9% on 2026-09-19 (44,482 of 149,008).",
    "18.2% of active postings had been live for more than 60 days on 2026-10-21, against 17.6% on 2026-09-19.",
    "7.9% of active postings had been live for more than 120 days on 2026-10-21, against 8.2% on 2026-09-19.",
    "13.9% of active postings belonged to a role repeated 5 or more times at the same company on 2026-10-21, against 13.7% on 2026-09-19.",
    "The median active posting with a believable posted date had been live 19 days on 2026-10-21, against 17 days on 2026-09-19.",
    "Among the 1,280 companies present in both snapshots, 30.8% of active postings carried a structural signal on 2026-10-21 (45,641 of 148,133), against 29.9% on 2026-09-19 (44,310 of 148,350).",
    "Among the same 1,280 companies, 18.3% of active postings had been live for more than 60 days on 2026-10-21, against 17.6% on 2026-09-19.",
    "48,636 of the 149,008 postings active on 2026-09-19 were still active on 2026-10-21 with the same company, normalized title and posted date.",
    "85,026 of the 149,008 postings active on 2026-09-19 had a posting at the same company under the same normalized title on 2026-10-21, whatever its posted date.",
    "48,636 of the 149,201 postings active on 2026-10-21 matched a 2026-09-19 posting on company, normalized title and posted date, and 26,291 of the 27,191 live for more than 60 days were among them.",
    "59.1% of the postings active on both dates carried a structural signal on 2026-10-21 (28,724 of 48,636), against 17% of the postings with no match in the 2026-09-19 file (17,081 of 100,565)."
  ],
  "population": {
    "totalActivePostings": 149201,
    "companies": 1343,
    "postingsWithUnknownPostedDate": 15,
    "postingsWithImplausiblePostedDate": 380,
    "postingsDatedAfterSnapshot": 0,
    "postingsDroppedForNullCompanyOrTitle": 0
  },
  "signals": {
    "liveOver60Days": {
      "count": 27191,
      "pct": 18.2
    },
    "liveOver120Days": {
      "count": 11853,
      "pct": 7.9
    },
    "distinctRolesRepeated5Plus": 1325,
    "distinctRolesRepeated12Plus": 313,
    "postingsInRolesRepeated5Plus": 20775,
    "anyStructuralSignal": {
      "count": 45805,
      "pct": 30.7
    },
    "overlap": {
      "ageSignalOnly": {
        "count": 25030,
        "pct": 16.8
      },
      "repeatSignalOnly": {
        "count": 18614,
        "pct": 12.5
      },
      "bothSignals": {
        "count": 2161,
        "pct": 1.4
      }
    }
  },
  "distributions": {
    "basis": "148,806 active postings with a believable posted_at",
    "medianDaysLive": 19,
    "p90DaysLive": 100,
    "daysLive": [
      {
        "label": "0 to 7 days",
        "min": 0,
        "max": 7,
        "count": 40593,
        "pctOfDatedPostings": 27.3
      },
      {
        "label": "8 to 30 days",
        "min": 8,
        "max": 30,
        "count": 58002,
        "pctOfDatedPostings": 39
      },
      {
        "label": "31 to 60 days",
        "min": 31,
        "max": 60,
        "count": 23020,
        "pctOfDatedPostings": 15.5
      },
      {
        "label": "61 to 120 days",
        "min": 61,
        "max": 120,
        "count": 15338,
        "pctOfDatedPostings": 10.3
      },
      {
        "label": "121 to 180 days",
        "min": 121,
        "max": 180,
        "count": 4202,
        "pctOfDatedPostings": 2.8
      },
      {
        "label": "Over 180 days",
        "min": 181,
        "max": null,
        "count": 7651,
        "pctOfDatedPostings": 5.1
      }
    ],
    "repeatCount": [
      {
        "label": "Listed once",
        "min": 1,
        "max": 1,
        "count": 106119,
        "pctOfActivePostings": 71.1
      },
      {
        "label": "Listed 2 to 4 times",
        "min": 2,
        "max": 4,
        "count": 22307,
        "pctOfActivePostings": 15
      },
      {
        "label": "Listed 5 to 11 times",
        "min": 5,
        "max": 11,
        "count": 6586,
        "pctOfActivePostings": 4.4
      },
      {
        "label": "Listed 12 or more times",
        "min": 12,
        "max": null,
        "count": 14189,
        "pctOfActivePostings": 9.5
      }
    ]
  },
  "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-19",
    "note": "None to the measurement. Thresholds, the epoch floor, title normalization and every signal definition are those of the 2026-09-19 snapshot, applied by the same code. Two things were added and neither changes a figure the 2026-09-19 file published. A 180-day count now appears in findings, computed from the same daysLive buckets that file already carried. The comparison block is new, and it is computed from the two published CSVs."
  },
  "comparison": {
    "priorSnapshot": {
      "quarter": "2026-Q3",
      "snapshotDate": "2026-09-19",
      "snapshotAt": "2026-09-19T18:26:39.327Z",
      "statsUrl": "https://four-leaf.ai/research/ghost-job-signals-2026-q3-stats.json",
      "datasetUrl": "https://four-leaf.ai/research/ghost-job-signals-2026-q3.csv"
    },
    "currentSnapshot": {
      "quarter": "2026-Q4",
      "snapshotDate": "2026-10-21",
      "snapshotAt": "2026-10-21T20:59:12.641Z"
    },
    "daysBetweenSnapshots": 32,
    "note": "Two snapshots, 32 days apart. They are two points, not a trend, and no rate per month is derived from them. Every share is over that snapshot's own denominator, which is printed beside it. changePctPoints is the Q4 share minus the Q3 share, in percentage points. The index is not a fixed panel, so the headline cut mixes a change in which companies are covered with a change in how covered companies post. The sameCompanyPanel cut holds the company set fixed. The carriedOver cut separates postings live on both dates from postings first seen in Q4.",
    "priorFileCheck": "The ghost-job-signals-2026-q3.csv file reproduces the active postings, companies, both age counts, both repeat-role counts, the repeat posting count, the any-signal count, the median and the 90th percentile in ghost-job-signals-2026-q3-stats.json exactly. This script refuses to run if it does not.",
    "headline": {
      "activePostings": {
        "q3": 149008,
        "q4": 149201
      },
      "companies": {
        "q3": 1319,
        "q4": 1343
      },
      "datedPostings": {
        "q3": 148529,
        "q4": 148806
      },
      "anyStructuralSignal": {
        "q3": {
          "count": 44482,
          "pct": 29.9
        },
        "q4": {
          "count": 45805,
          "pct": 30.7
        },
        "changePctPoints": 0.8
      },
      "liveOver60Days": {
        "q3": {
          "count": 26249,
          "pct": 17.6
        },
        "q4": {
          "count": 27191,
          "pct": 18.2
        },
        "changePctPoints": 0.6
      },
      "liveOver120Days": {
        "q3": {
          "count": 12179,
          "pct": 8.2
        },
        "q4": {
          "count": 11853,
          "pct": 7.9
        },
        "changePctPoints": -0.3
      },
      "liveOver180Days": {
        "q3": {
          "count": 7485,
          "pctOfDatedPostings": 5
        },
        "q4": {
          "count": 7651,
          "pctOfDatedPostings": 5.1
        },
        "changePctPoints": 0.1
      },
      "postingsInRolesRepeated5Plus": {
        "q3": {
          "count": 20467,
          "pct": 13.7
        },
        "q4": {
          "count": 20775,
          "pct": 13.9
        },
        "changePctPoints": 0.2
      },
      "distinctRolesRepeated5Plus": {
        "q3": 1360,
        "q4": 1325
      },
      "distinctRolesRepeated12Plus": {
        "q3": 308,
        "q4": 313
      },
      "medianDaysLive": {
        "q3": 17,
        "q4": 19
      },
      "p90DaysLive": {
        "q3": 101,
        "q4": 100
      }
    },
    "sameCompanyPanel": {
      "definition": "Companies present, under the same company string, in both snapshots. Repeat counts are computed within a company, so restricting to these companies leaves every posting's repeat count as published.",
      "activePostings": {
        "q3": 148350,
        "q4": 148133
      },
      "companies": {
        "q3": 1280,
        "q4": 1280
      },
      "datedPostings": {
        "q3": 147871,
        "q4": 147738
      },
      "anyStructuralSignal": {
        "q3": {
          "count": 44310,
          "pct": 29.9
        },
        "q4": {
          "count": 45641,
          "pct": 30.8
        },
        "changePctPoints": 0.9
      },
      "liveOver60Days": {
        "q3": {
          "count": 26092,
          "pct": 17.6
        },
        "q4": {
          "count": 27053,
          "pct": 18.3
        },
        "changePctPoints": 0.7
      },
      "liveOver120Days": {
        "q3": {
          "count": 12135,
          "pct": 8.2
        },
        "q4": {
          "count": 11822,
          "pct": 8
        },
        "changePctPoints": -0.2
      },
      "liveOver180Days": {
        "q3": {
          "count": 7459,
          "pctOfDatedPostings": 5
        },
        "q4": {
          "count": 7637,
          "pctOfDatedPostings": 5.2
        },
        "changePctPoints": 0.2
      },
      "postingsInRolesRepeated5Plus": {
        "q3": {
          "count": 20451,
          "pct": 13.8
        },
        "q4": {
          "count": 20748,
          "pct": 14
        },
        "changePctPoints": 0.2
      },
      "distinctRolesRepeated5Plus": {
        "q3": 1357,
        "q4": 1321
      },
      "distinctRolesRepeated12Plus": {
        "q3": 308,
        "q4": 313
      },
      "medianDaysLive": {
        "q3": 17,
        "q4": 19
      },
      "p90DaysLive": {
        "q3": 101,
        "q4": 100
      }
    },
    "companyChurn": {
      "definition": "Companies present in one snapshot only, with the headline figures over their postings. A company string that changed spelling between snapshots appears in both lists.",
      "onlyInPrior": {
        "companies": 39,
        "activePostings": 658,
        "anyStructuralSignal": {
          "count": 172,
          "pct": 26.1
        },
        "liveOver60Days": {
          "count": 157,
          "pct": 23.9
        }
      },
      "onlyInCurrent": {
        "companies": 63,
        "activePostings": 1068,
        "anyStructuralSignal": {
          "count": 164,
          "pct": 15.4
        },
        "liveOver60Days": {
          "count": 138,
          "pct": 12.9
        }
      }
    },
    "carriedOver": {
      "definition": "Postings active on 2026-10-21 whose company, title_norm and posted_at all match a row in ghost-job-signals-2026-q3.csv, matched one for one. Neither CSV carries ids, so this is a match on published columns. A listing that was taken down and re-posted with its original posted_at counts as carried over, and one re-posted with a new posted_at, retitled, or listed under a changed company string counts as first seen. Postings with no posted_at never match. Postings whose posted_at is below the epoch floor match on the raw value like any other.",
      "carriedOver": {
        "activePostings": 48636,
        "pctOfActivePostings": 32.6,
        "liveOver60Days": {
          "count": 26291,
          "pct": 54.1
        },
        "anyStructuralSignal": {
          "count": 28724,
          "pct": 59.1
        },
        "postingsInRolesRepeated5Plus": {
          "count": 4586,
          "pct": 9.4
        }
      },
      "firstSeen": {
        "activePostings": 100565,
        "pctOfActivePostings": 67.4,
        "liveOver60Days": {
          "count": 900,
          "pct": 0.9
        },
        "anyStructuralSignal": {
          "count": 17081,
          "pct": 17
        },
        "postingsInRolesRepeated5Plus": {
          "count": 16189,
          "pct": 16.1
        }
      },
      "shareOfLiveOver60DaysCarriedOver": 96.7,
      "priorPostingsMatched": {
        "count": 48636,
        "of": 149008,
        "pct": 32.6,
        "note": "The same matched postings, as a share of the 2026-09-19 snapshot. A 2026-09-19 posting with no match may have been taken down, listed again under a different posted_at, retitled, or listed under a changed company string. The match cannot say which, so an unmatched posting is never evidence that a job was filled or removed."
      },
      "priorPostingsMatchedOnCompanyAndTitle": {
        "count": 85026,
        "of": 149008,
        "pct": 57.1,
        "note": "2026-09-19 postings with a 2026-10-21 posting at the same company under the same normalized title, matched one for one and ignoring posted_at. It is a looser match rule than priorPostingsMatched, not a bound. A retitled or renamed posting that stayed listed is in neither count, and a listing taken down and re-posted under its original posted_at is in both, so neither number is the share still listed and neither is an exit rate."
      }
    }
  },
  "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-q4.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;"
  }
}
