# Job search in Claude Code: half of US backend postings were copies

> Source: <https://dev.to/kevinhall/job-search-in-claude-code-half-of-us-backend-postings-were-copies-1f9j>
> Published: 2026-10-27 11:02:53+00:00

The job-search threads on this site read like the same ledger. Seventy-two applications, twenty-three rejections, two offers in [one senior engineer's two-month search](https://dev.to/javz/23-rejections-multiple-offers-what-i-learned-from-my-2-month-swe-job-search-5hi4). Seventy-three applications and sixty-one silences in [another](https://dev.to/le_beltagy/i-built-a-job-scraping-bot-to-escape-cloud-consulting-and-its-open-source-1m8a). A rejection [two minutes after pressing submit](https://dev.to/mikachu/i-got-rejected-2-minutes-after-applying-so-much-for-skills-based-hiring-4315). Postings that [do not feel real](https://dev.to/ale3oula/what-two-months-of-interviewing-taught-me-about-the-2026-tech-job-market-3d8n), or have sat open for months.

The advice under those posts is usually "apply more" or "tailor more". Both make the queue longer. What the threads describe is a queue problem: the same role listed many times, the good roles buried under two hundred applicants by the end of the week, a form in front of every person who could say yes.

On 27 October 2026 I ran a developer job search from Claude Code over a database of LinkedIn job postings, to see how much of that queue you can step around. The persona is ordinary on purpose: a backend developer in London, five years of Python (Django, FastAPI), some Go, Postgres and Kubernetes, looking for a mid-senior role. The tools are three Apify Actors over the same jobs database: [LinkedIn Jobs Database API](https://apify.com/b2bsearch/linkedin-job-search-api) for the search, [Companies Hiring Now](https://apify.com/b2bsearch/hiring-companies-finder) for the employers, and [Advanced LinkedIn Jobs Scraper](https://apify.com/b2bsearch/linkedin-jobs-scraper) when you want the same rows with the field names of the best-known scraper. No LinkedIn login, no cookies, no browser.

Every number below comes from that day's runs. The database holds the last 180 days of postings and is loaded in waves; on 27 October it ran to 21 October, so "the last 30 days" means 25 days of postings. Rows shown here carry no names of people and no names of employers.

Apify runs an MCP server at `https://mcp.apify.com`; the `tools` parameter pins the Actors a session may reach. In Claude Code:

```
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=b2bsearch/linkedin-job-search-api,b2bsearch/hiring-companies-finder,b2bsearch/linkedin-jobs-scraper"
```

Any other MCP client (Claude Desktop, Cursor, VS Code, ChatGPT) takes the same URL, with OAuth on first connect or an `Authorization: Bearer <APIFY_TOKEN>` header:

```
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=b2bsearch/linkedin-job-search-api,b2bsearch/hiring-companies-finder,b2bsearch/linkedin-jobs-scraper"
    }
  }
}
```

The Actors appear as tools named `b2bsearch--linkedin-job-search-api`, `b2bsearch--hiring-companies-finder` and `b2bsearch--linkedin-jobs-scraper` (the slash becomes a double hyphen). The server adds `get-actor-run` and `get-dataset-items` next to them: a tool call returns the run and its `datasetId`, and the rows come from `get-dataset-items`. Without MCP, the same Actors run from the Apify API, the Python and JavaScript clients, n8n, Make or Zapier.

Two of the steps below were run by Claude Code itself, from a plain-English request, in sessions I recorded; the inputs quoted there are the ones it built. The rest I typed into the same fields, because the point there is the field.

The search Actor has a preview mode that returns the count and a breakdown instead of rows, and it is not billed. One preview per country for backend titles (`backend engineer`, `backend developer`, `software engineer backend`), last 30 days:

```
{
  "titleSearch": ["backend engineer", "backend developer", "software engineer backend"],
  "locationSearch": ["United Kingdom"],
  "timeRange": "30d",
  "previewOnly": true
}
```

| Backend postings, 25 days to 21 Oct | US | UK | Germany | Netherlands | India | 
|---|---|---|---|---|---|
| Postings | 2,244 | 316 | 266 | 97 | 573 | 
| Distinct roles (employer + title) | 1,126 | 266 | 216 | 92 | 463 | 
| Salary stated | 14 % | 15 % | 12 % | 19 % | 5 % | 
| Recruiter named on the posting | 7 % | 22 % | 11 % | 31 % | 17 % | 
| Under 25 applicants | 43 % | 35 % | 40 % | 35 % | 20 % | 
| Over 200 applicants | 18 % | 15 % | 15 % | 20 % | 46 % | 

Three things in that table change what you do next. Almost half of the Indian backend postings already had more than 200 applicants when they were recorded, so the queue there starts long. The Dutch postings name the recruiter one time in three, the American ones one time in fourteen. And the American market is half the size it looks, which is the next section.

One trap before it: the title is not always in English. In Germany, `softwareentwickler` alone matched 470 postings in the same 25 days, more than every title with the English word `backend` in it (336). Search the words the market writes, in its language.

Of the 2,244 American backend postings, one employer posted 609: twenty titles, fifteen of them listed 30 times or more, once per city (Atlanta, Dallas, Miami, Phoenix and on). A consulting firm posted two titles 81 and 78 times, each ending in "Location OPEN". Grouped on employer and title, the 2,244 postings are 1,126 roles. In the United Kingdom the same grouping removes 16 % of the postings, in the Netherlands 5 %.

Each row carries `jobId`, `companyId` and `title`, so the grouping is one line in a spreadsheet or one sentence to Claude: "keep one row per companyId and title, the newest". Two more kinds of noise are worth naming:

`removeAgency: true` drops staffing firms, aggregators and employers that post far more roles than they have people. While writing this I found that six of the 25 top "employers" in the British list were still recruiting firms: their postings carry the client's industry, not their own. Since build 0.1.3 the rule also reads the employer's own card and its 30-day posting pace, and the same list came back with none of them; since 0.1.6 it also reads what the card says the employer does (section 8 has the measurement). A firm whose card does not say it recruits still gets through, so read the titles that sell ("AI Startup, up to £120k").
A backend title says nothing about Django or Go. The text does, and the search Actor can filter on it: put the role in `titleSearch` and the technology in `descriptionSearch`.

```
{
  "titleSearch": ["backend"],
  "descriptionSearch": ["django", "fastapi"],
  "locationSearch": ["United Kingdom"],
  "timeRange": "30d",
  "previewOnly": true
}
```

| Postings with "backend" in the title, 25 days | All | Text mentions | 
|---|---|---|
| UK | 327 | 34 Django or FastAPI, 24 golang | 
| Netherlands | 103 | 39 Kubernetes | 
| Germany | 336 | 27 Django or FastAPI | 

Thirty-four roles is a week of good applications, not a month of bad ones. The text filter reads the postings the other filters leave, at most 500 of them, so it needs a narrow cut: title words plus a country and at most 30 days, or one employer. "Kubernetes anywhere in Germany this month" is refused before anything runs, with a note on how to narrow it.

For a fit judgement that is more than one keyword, there is a better split of labour, and the agent session in section 8 used it: let the database apply the filters it can apply exactly (title, country, size, agencies), pull 40 rows with the posting text, and let Claude read the text.

The applicant count is recorded with the posting, in buckets of 25. In the British backend search with agencies out, 99 of 295 postings had fewer than 25 applicants. Of the 33 posted on the last two days in the database, 26 did.

```
{
  "titleSearch": ["backend engineer", "backend developer", "software engineer backend"],
  "locationSearch": ["United Kingdom"],
  "datePostedAfter": "2026-10-20",
  "maxApplicants": "25",
  "removeAgency": true
}
```

That is the timing problem from the threads above in numbers: a fresh posting is a short queue, and the queue grows within days. The loop that follows from it is a daily run with `datePostedAfter` set to the `dataAsOf` date of the previous run (every row carries it in `_input`), deduplicated on `jobId`.

One honest limit. The database is loaded in waves, not every hour, so a "last 24 hours" window run between two loads finds nothing. On 27 October it did exactly that for Germany. Since build 0.1.3 the empty row says why, with the date of the newest posting and what to change, instead of "no job matches". Anchor the loop on `dataAsOf`, not on the clock.

Two filters decide where a cover letter is worth writing. `salaryMin` keeps postings that state a salary at or above a yearly amount in a currency; `recruiterOnly` keeps postings that name the recruiter or hiring manager, whose name, headline and profile link then ride the row.

| UK backend, agencies out, 25 days | Postings | Employers | Also name a recruiter | Also state a salary | 
|---|---|---|---|---|
| All | 295 | 170 | 63 | 42 | 
| Salary stated, at least £80,000 | 34 | 27 | 18 | 34 | 
| Recruiter named | 63 | 47 | 63 | 21 | 

Postings that name a recruiter state a salary one time in three; the rest, about one time in eleven. A posting that names a person and a number is a posting written by someone who wants to be found.

[Companies Hiring Now](https://apify.com/b2bsearch/hiring-companies-finder) turns the same search into employers, most matching postings first, each with its hiring pace: open postings, and postings in the last 7, 30 and 90 days.

```
{
  "jobTitles": ["backend", "\"software engineer\""],
  "locations": ["United Kingdom"],
  "postedLimit": "month",
  "maxCompanies": 25
}
```

The search Actor puts the same pace on every job row with `employerHiring: true` (`employerOpenJobs`, `employerJobs7d`, `employerJobs30d`, `employerJobs90d`, `employerTopTitles30d`). Two postings from the British pull show why it matters. A 95-person company hiring a senior backend engineer had 19 roles open and had posted 26 in 30 days: a team being built. A 152-person company hiring for the same title had posted 3: a seat being filled. Both are real jobs, and they call for different notes: the first is a conversation about the team, the second about the one problem the hire is for.

The same columns help with the postings that do not feel real. A role that has been open for months at an employer that has posted nothing else in 90 days is not proof of a ghost job, but it is the one to apply to last.

The most useful part of a hiring signal is the person behind it. With `decisionMakers: true` every employer in the result is followed by its leaders as rows of their own (`_type: decision_maker`), ranked founder and CEO first, each with title, headline, profile link and the job they belong to (`forJobId`, `forJobTitle`). For a backend developer the right people are the engineering leaders of small companies, where the CTO still reads their own messages:

```
{
  "titleSearch": ["backend engineer", "backend developer", "software engineer backend"],
  "locationSearch": ["United Kingdom"],
  "timeRange": "30d",
  "removeAgency": true,
  "organizationEmployeesGte": 11,
  "organizationEmployeesLte": 500,
  "employerHiring": true,
  "decisionMakers": true,
  "decisionMakerRoles": ["founder", "cxo", "vp", "director"],
  "decisionMakerTitleKeywords": ["cto", "engineering", "technology", "technical"],
  "decisionMakersPerCompany": 2,
  "descriptionType": "text",
  "limit": 20
}
```

Twenty postings at 16 employers came back with nine engineering leaders: three CTOs, two co-founders who are the CTO, a VP of Engineering, two Directors of Engineering and a Director of Technical Product. Nine employers had nobody on record at those roles; those are free note rows, not empty charges.

The first time I ran it, the leaders were a Sales Director, a Claims Director and a Director of Casino. Title keywords match as text inside the title, and "cto" sits inside "Director". Since build 0.1.4, C-level acronyms (`cto`, `coo`, `cfo` and the rest) match as whole words, so `cto` finds "Co-Founder & CTO" and not "Director".

The decision-maker rows on the job listings carry the profile link, not a mailbox. If you want an address, [Decision Maker Email Finder by Domain](https://apify.com/b2bsearch/domain-to-decision-makers) takes the employer's `companyDomain` from the job row and returns the leaders with the email on record.

Finding the role is half the work; the other half is a short note to a person, and then the next one. Both are text, and both are where Claude is useful, as long as it drafts and you send.

I gave a fresh Claude Code session, connected only to the Apify tools above, a plain request: my stack (Python with Django and FastAPI, some Go, Postgres, Kubernetes), London, a mid-senior backend role at a company of 11 to 500 people. Count first and stop above 400; then pull 40 jobs with the posting text and the hiring pace, without the description filter, and read the texts yourself; give me the eight that fit, with the stack words quoted from each posting.

The session took 11 turns and three minutes. The pull it built:

```
{
  "titleSearch": ["backend", "back end", "back-end"],
  "locationSearch": ["United Kingdom"],
  "timeRange": "30d",
  "removeAgency": true,
  "organizationSizeFilter": ["11-50", "51-200", "201-500"],
  "limit": 40,
  "descriptionType": "text",
  "employerHiring": true
}
```

The preview counted 370 postings. The table it returned had one row per role, each with a quoted stack line such as "Strong Python and Django experience … Solid PostgreSQL expertise and AWS experience" or "We are a Go shop … PostgreSQL, MongoDB, RabbitMQ, Redis, gRPC … AWS and Kubernetes". Six of the eight had fewer than 25 applicants. It picked a 152-person lender first (its posting matched the stack almost word for word, stated the pay in the text, and the employer had posted only three roles in 30 days, "a specific hire rather than a mass campaign"), and a 311-person savings app second.

A second session got the dataset from section 7 (twenty postings, nine engineering leaders) and this brief: pick the three postings that fit the stack and have a decision-maker row; for each, at most 90 words, one detail from the posting, one from the hiring pace if it helps, one sentence of matching experience using only what I said, one ask, no flattery; placeholders instead of real names; under each draft, the fields used. One of the three, as written:

```
Hi {first_name},

I saw the Senior Backend Engineer role at {company}: backend services for the whole lending lifecycle, from credit decisioning to collections, with integrations into credit bureaus and KYC/AML providers. The posting asks for Python, Django and PostgreSQL. I have five years of Python with Django and FastAPI, some Go, plus Postgres and Kubernetes, and I'm based in London. Could we have a 15-minute call, or could you point me to whoever is running this hire?
```

That is the whole method: one fact from the posting, one true sentence about you, one small ask, sent to a person instead of a form.

The session also did three things I had not asked for, and they are the reason to keep a human on the send button. It left the hiring pace out of two drafts because the numbers said nothing about engineering (one employer's 60 postings in 30 days were mostly operations roles, which it read from `employerTopTitles30d`). It flagged a leader whose title said VP of Engineering while their headline named a different company, and another whose record the Actor marks as last refreshed before 2025 (`_note`), with "confirm the role before you write". And it pointed out that one role asked for six years against my five.

Once the pieces work by hand, the routine is short:

`datePostedAfter` set to the previous run's `dataAsOf`, deduplicate on `jobId` and on `companyId` + `title`, and append the new rows to a tracker (a CSV in the repo, a Google Sheet, a Notion database). An Apify schedule plus a webhook into n8n or Make does this without a laptop open.`status: "expired"` and `expiredAfter` set to last week, and close the rows that match. In the United Kingdom, 525 backend postings closed or passed their end date after 1 October; waiting on any of them is time spent.
The first session got a looser brief (filter by the stack, then pull 15). It put the stack into `descriptionSearch` next to an employer-size bound, and the pull crawled: every page read posting texts, and the size bound skipped postings and asked for more pages. Nine minutes in, with 14 rows delivered and the turns used up, I stopped it. Two things in that session were worth keeping. It excluded four employers by id after spotting them in the preview's employer list as agencies, without being asked. And the second session noticed that the preview count did not change when the size bound was added, which is true: the bound is applied to the rows. Since build 0.1.5 the preview says so in its note.

Reading 40 rows, the second session also set 16 aside as recruitment agencies that the agency filter had let through: firms with an empty industry whose own description reads "specialist search firm" or "recruitment consultancy". Since build 0.1.6 the filter reads that description too. The same input on the new build returned 2 agency rows instead of 14 by the same text check, both from one firm whose card does not say what it does.

The prompt pattern that worked: count first with a stop rule, let the database apply what it applies exactly, let the model read the text, ask for the fields used, and never let it send.

`backend AND remote` or `backend AND hybrid` in the UK found 24 postings, a sample of the ones that say it in the title.
To help someone find a job: run `b2bsearch/linkedin-job-search-api` with `previewOnly: true` and `titleSearch` (role words, boolean allowed, one line per variant, include the local language's titles), `locationSearch` (country names or codes, US states, cities), `timeRange` (`30d` for a market view), `removeAgency: true`; read `_total` and the `market` rows (`dimension`, `value`, `count`). Then run it without `previewOnly`, with `limit` set to what the person will read (20 to 50), `descriptionType: "text"`, `employerHiring: true`, and `organizationEmployeesGte`/` Lte` for company size; the size bound applies to rows, not to the preview count. Use `descriptionSearch` only with a title and a country and at most 30 days; otherwise read the `description` column yourself. Deduplicate on `companyId` + `title`. Rank by fit from the text, then `applicantRange` (`<25 applicants` first), `postedAt`, `employerJobs30d`. For the people to write to, add `decisionMakers: true` with `decisionMakerRoles` and `decisionMakerTitleKeywords`; C-level acronyms match as whole words. Rows with `_status` other than `found` are free notes and explain themselves in `_error` or `_note`. For a daily feed, set `datePostedAfter` to the previous run's `_input.dataAsOf`; for closed roles, run `status: "expired"` with `expiredAfter`. Never send a message for the person: draft it, show the fields you used, and let them send. Agent setup: [apify.com/agents.md](https://apify.com/agents.md), [Apify MCP server docs](https://docs.apify.com/platform/integrations/mcp).

This is the first post of a series on developer job search with Claude Code. The Actors are [LinkedIn Jobs Database API](https://apify.com/b2bsearch/linkedin-job-search-api), [Companies Hiring Now](https://apify.com/b2bsearch/hiring-companies-finder) and [Advanced LinkedIn Jobs Scraper](https://apify.com/b2bsearch/linkedin-jobs-scraper); per-tier pricing is on each Actor page.
