DEV Community

Cover image for Find people asking for what you sell, using LinkedIn post search
Andrew
Andrew

Posted on

Find people asking for what you sell, using LinkedIn post search

The best lead you will ever get is someone publicly asking for the thing you sell.

Not a scraped list. Not a cold list bought from a broker. An actual person, this week, writing "can anyone recommend a good X" to their network — and waiting for replies.

Those posts exist constantly. The problem is that LinkedIn's search box is built for browsing, not monitoring: you cannot schedule it, diff it against yesterday, or get the results out. So people either check manually until they get bored, or never look at all.

This is a small pipeline that does the looking.

Intent lives in a handful of phrases

Buying intent on LinkedIn is remarkably formulaic. Almost all of it shows up as one of:

  • "can anyone recommend"
  • "looking for a tool"
  • "any alternatives to"
  • "we're hiring" (intent for recruiters)
  • "does anyone use"
  • "switching from"

That last one is the sharpest. Someone saying "switching from X" is a person mid-decision, with the incumbent named for you.

The trick is that these are phrases, not topics. Searching "CRM" gets you a firehose of marketing. Searching "any alternatives to" gets you a short list of people with a live problem.

Pulling the posts

I'm using an Apify Actor that runs LinkedIn's post keyword search and returns structured results. No login, no cookie to paste from your browser, no account to connect.

curl -X POST \
  "https://api.apify.com/v2/acts/data_pool~linkedin-post-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "keywords": ["any alternatives to", "can anyone recommend"],
    "maxItems": 100,
    "datePosted": "24h",
    "sortBy": "date_posted"
  }'
Enter fullscreen mode Exit fullscreen mode

Each keyword runs as its own search and results are de-duplicated across them, so overlapping phrases do not produce duplicate rows.

What comes back

{
  "postUrl": "https://www.linkedin.com/posts/ias07_generativeai-...",
  "postUrn": "urn:li:activity:7...",
  "text": "Generative AI is moving beyond simple conversations...",
  "postedAtIso": "2026-08-16T10:39:56.510Z",
  "author": {
    "name": "Jane Doe",
    "headline": "VP Sales @ Acme",
    "profileUrl": "https://www.linkedin.com/in/jane-doe",
    "type": "person"
  },
  "stats": { "reactions": 12, "comments": 0, "reposts": 0, "impressions": 0 },
  "isRepost": false,
  "matchedKeyword": "any alternatives to"
}
Enter fullscreen mode Exit fullscreen mode

Two fields do most of the work downstream:

  • author.headline is your qualifier. It is the single cheapest way to tell a VP of Engineering from a student, without another API call.
  • author.type is person or company. Company pages post too, and they are almost never the lead you want.

Also worth knowing: stats.impressions is often 0. LinkedIn only exposes impressions on some posts, so treat it as a bonus rather than a field you can sort by.

Filtering down to the ones worth reading

Raw keyword matches still contain a lot of noise — recruiters quoting the phrase, people describing what they built, reposts of someone else's question. A few cheap rules cut most of it.

import os
import re
import requests

ACTOR = "data_pool~linkedin-post-scraper"
URL = f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items"

INTENT = [
    "any alternatives to",
    "can anyone recommend",
    "looking for a tool",
    "does anyone use",
    "switching from",
]

# Headlines that are almost never the buyer for a B2B tool.
EXCLUDE_HEADLINE = re.compile(
    r"\b(student|intern|seeking|open to work|recruiter|talent acquisition)\b",
    re.I,
)


def search(keywords, hours="24h", limit=100):
    resp = requests.post(
        URL,
        headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"},
        json={
            "keywords": keywords,
            "maxItems": limit,
            "datePosted": hours,
            "sortBy": "date_posted",
        },
        timeout=300,
    )
    resp.raise_for_status()
    return resp.json()


def worth_reading(post):
    author = post.get("author") or {}
    if author.get("type") != "person":
        return False                       # company pages aren't leads
    if post.get("isRepost"):
        return False                       # they're amplifying, not asking
    if EXCLUDE_HEADLINE.search(author.get("headline") or ""):
        return False
    return True


for post in search(INTENT):
    if not worth_reading(post):
        continue
    author = post["author"]
    text = " ".join((post.get("text") or "").split())
    print(f"{author['name']}{author.get('headline', '')}")
    print(f"  \"{text[:160]}\"")
    print(f"  {post['postUrl']}")
    print(f"  matched: {post['matchedKeyword']}  ·  {post['stats']['comments']} comments\n")
Enter fullscreen mode Exit fullscreen mode

The isRepost check matters more than it looks. A repost means that person is amplifying someone else's question, not asking one — and reposts are a large share of what any keyword search returns.

The second, better use: read the replies

Here is the part most people miss.

When someone posts "can anyone recommend a tool for X", the interesting audience is not only the author. It is everyone who replied. Those people either have an opinion about the category or the same problem. A post with 40 comments asking for recommendations is 40 qualified people, plus one.

So the pipeline extends naturally: take the postUrl values with a healthy stats.comments count, and pull everyone who engaged with them using a post engagement Actor. You go from "someone asked about my category" to "here are the fifty people in that conversation, with profile links."

high_signal = [
    p["postUrl"] for p in search(INTENT)
    if worth_reading(p) and p["stats"]["comments"] >= 5
]
# feed high_signal into the engagement Actor to get the participants
Enter fullscreen mode Exit fullscreen mode

That is a fundamentally different list from a scraped export. Every person on it opted into a conversation about the problem you solve, in public, this week.

Running it daily

Keep it to the last 24 hours and schedule it. A monitoring query wants recency, not the all-time best posts:

  • datePosted: "24h" and sortBy: "date_posted" so each run only sees new material
  • cron or GitHub Actions on a schedule, token in a secret
  • Push the digest somewhere you already look — Slack DM beats a log file you forget to open
  • Deduplicate on postUrn, which is stable across runs

Other things the same query shape is good for

  • Competitor monitoring — search their brand name, and you get both praise and complaints. The complaints are leads.
  • Content research — drop the date filter, sort by relevance, and rank by stats.reactions to see what actually resonates in a niche before writing your own post.
  • Recruiting signals — "we're hiring" plus a role keyword surfaces teams growing right now, often before the job posting appears.
  • Community discovery — the same names recurring across a topic's posts are that topic's actual practitioners.

What it costs

$1.50 per 1,000 posts returned, no subscription and no per-run fee.

A daily monitoring run pulling 100 posts is 3,000 posts a month — about $4.50, most or all of which Apify's free monthly credit absorbs. Intent monitoring is cheap because the useful queries are narrow by construction.

The honest caveats

  • LinkedIn caps what any search returns — realistically a few hundred results per query, not everything ever posted. Several narrow phrase searches beat one broad topic search, which is also better for signal.
  • Results are not deterministic. Ranking shifts between runs, so a daily digest is "what surfaced today", not a complete index. Use recency sorting if you need runs to be comparable.
  • Phrase matching is fuzzy. You will get posts that contain your phrase in a different sense. The filters above help; nothing removes it entirely.
  • stats.impressions is frequently 0 — available for some posts only.
  • These are real people. Names, headlines and profile URLs are personal data under GDPR and similar laws, and public visibility is not the same as unrestricted use. Have a lawful basis, and if you reach out, reference the actual post — replying to a real question is legitimate; blasting a scraped list is what gives this whole category a bad name.

Wrapping up

The shift that made this useful was searching for phrases that indicate a decision rather than topics. "CRM" is a firehose. "any alternatives to" is a queue of people with a live problem and a deadline.

And once you have those posts, the comments are where the actual audience is.

Top comments (0)