DEV Community

Cover image for The same ghost job is charging you three times
Ava Bagherzadeh
Ava Bagherzadeh

Posted on

The same ghost job is charging you three times

You open a listing and something about it feels familiar. The title, the bullet about "fast-paced environment", the same slightly-off salary band. You check your tracker. You applied to this in March.

Except you didn't, not to this one. This is a different URL, on a different board, posted eleven days ago.

It is the same job.

Syndication is the part nobody explains

When a company posts a role, it rarely stays in one place. The ATS pushes it to the company careers page, then to the aggregators, then the aggregators repost each other. Each hop adds its own tracking parameters:

https://boards.example.com/jobs/1234?utm_source=indeed&utm_campaign=q3&gh_src=abc
https://www.example.com/careers/1234?ref=linkedin
https://aggregator.example/j/1234?s=email&t=daily
Enter fullscreen mode Exit fullscreen mode

Three URLs. One job. Your tracker sees three rows, because a spreadsheet compares strings and those strings are different.

So when the role is a ghost — a listing kept open to collect a pipeline, or one that was filled internally in week one — it doesn't cost you one application. It costs you every time you meet it again and don't recognise it.

The check you can run on your own tracker tonight

Take your spreadsheet. Add a column. For each URL, strip everything from the ? onwards, and lowercase what's left. Sort by that column.

=LOWER(LEFT(A2, IFERROR(FIND("?", A2) - 1, LEN(A2))))
Enter fullscreen mode Exit fullscreen mode

Count the duplicates. Most people are surprised, and the number is worse for anyone applying through aggregators rather than company pages.

That's it. No tool required, and it will tell you something your row count has been hiding.

Why I did not build ghost-job detection

I run a product in this space, so the obvious feature request lands in my inbox constantly: filter out the fake postings.

We have the code for it. A scoring service, several modules, tiers, a cross-check against layoff news. It is not wired into the apply path, and I left it that way deliberately.

When I looked at what the score actually measured, it was recency, whether a salary was disclosed, and description length. That's a proxy for "this listing looks well maintained". A ghost job is defined by intent, and none of those three observe intent. A well-maintained ghost job scores well.

The tell was that I could not write a test that would fail if the feature were wrong. If you cannot construct the failing case, you have not built a check. You have built a number that goes up.

What actually helps

A uniqueness constraint, which is a much duller thing.

Normalise the URL, strip the tracking parameters, and enforce a unique index on the pair of (user, normalised posting). Then the same job syndicated across four boards collapses into one record, and the second attempt is rejected by the database rather than by a judgement call.

Detection is a claim. Deduplication is a constraint. A claim asks you to trust that someone's scoring is good. A constraint is enforced by something that cannot be reasoned around, and when it fails you get an error instead of a quietly wrong answer.

It does something far smaller than detecting ghost jobs. It means a posting that was never real costs you once instead of three times. That happens to be the difference you can actually feel at the end of a month.

The general version

I have started asking it of anything that sounds intelligent: is there an index or a uniqueness rule underneath this, or is it a model output wearing a confident label?

Often the honest version is smaller, duller, and works.


I build AI Applyd. It applies to jobs for you across the twelve ATS platforms we cover, folds tracking parameters so the same posting is one posting, and counts an application as sent only when the employer's own system confirms it.

Top comments (0)