The real distance of automated lead-hunting: four days, 638 posts, for one reply seen by three people
I spent an afternoon building a radar that hunts for freelance leads automatically. Four days later I came back to settle the accounts: it ran on schedule every day, judged cleanly, never missed a notification, and the leads it found were real. Then I pulled the stats on the one reply I actually posted — 3 views.
It started with a single post.
Someone put out a call on Threads: their company wanted a booking automation system — system integration, AI-powered reply filtering, slot scheduling, conflict exclusion, automated reminders — and asked whether any engineer or IT firm could quote.
Thirty-odd replies underneath. Open them up and most read like this: "Hi, we're ⋯ we offer AI customer service and language model training services, please take a look at our company website, hope we get the chance to discuss your needs."
Mine included.
I stared at those thirty-odd canned replies for a while, and what I thought wasn't "I need to post faster" — it was "this whole thing is being done wrong." So I spent an afternoon building a radar. Here's how it went, including four failures I briefly assumed were someone else's fault and which turned out to be entirely self-inflicted.
1. The first road is a dead end
Let's start with the technical route, because most people's instinct is "just use the official API."
Meta's Threads does expose a /keyword_search endpoint that searches all public posts site-wide, and the docs even state outright: you can reply to, quote, and repost whatever you find. That's exactly what I wanted, and the quotas are generous — 2,200 searches a day, 250 posts a day.
I fired an existing token at it and got this back:
{"error":{"message":"Application does not have permission for this action","code":10}}
To use that endpoint, you need the threads_keyword_search permission, and that permission has to clear Meta's App Review: screen recordings, possibly business verification, one to two weeks minimum. Without approval, the endpoint only returns "posts you published yourself" — which is useless.
So the official route is worth pursuing, but not worth waiting for. The only thing I could ship that same day was browser automation: open a browser and read the search results page like a human would.
(Plain-language note: browser automation means writing code that drives a real browser — what to click and what to read is decided by the program. The downside is that a site redesign breaks it; the upside is that you don't have to wait for anyone's approval.)
2. I decided against full automation
This was the most important decision in the whole project, and it had nothing to do with technology.
The original spec was "find leads automatically, reply automatically." Doable — but I cut it down to "find leads automatically, draft the reply automatically,human decides whether to post." Three reasons:
First, the platform will block you. I'd previously built another Threads bulk-commenting tool; a six-second interval was enough to trip Meta's anti-spam detection, error code 4279009, and I had to stretch the interval to 75 seconds to get through. Dropping the same pitch under a string of strangers' posts is exactly the behavioral pattern these systems are most sensitive to. Once your account is throttled, even your normal posts get no reach.
Second, canned replies don't work. Those thirty-odd replies are the proof. Under a popular open call, what gets noticed isn't "we offer these services" — it's the one sentence the poster hadn't thought of.
Third, early on you have no idea what needs tuning. Whether the keywords pull the right things, whether the scoring is too strict — you only find out by looking at real results. If you can't see them, you can't tune them.
So the final shape is: sweep every three hours, and once the judging is done, push the high scorers plus a ready-written draft reply to my Telegram. I glance at it on my phone, copy it, edit if needed, and post it myself.
3. The time sink wasn't the scraper — it was writing down "what we don't take"
The scraper took about twenty minutes. The judging — deciding whether a post is worth replying to — is the core, and its quality depends entirely on one document.
I pulled together three pages from our website, the raw config file for our service pricing, and the projects I've done over the past few years into a capability brief. It has prices (single-page site from NT$9,000, multi-page from NT$18,000, bots from NT$50,000 ⋯), it has case work I can point to as proof, it has scoring rules, and it has a "won't take" list:
One-off pure graphic design, hardware/firmware, blockchain gambling, long-term on-site staffing, unpaid pitches, budgets clearly below threshold, portfolio-for-work trades, pitches from other agencies, and pure venting or industry commentary.
That list is the most valuable thing in the entire system.
Because what AI lacks has never been intelligence. It reads Chinese fine, it can tell tone, it can tell who's hiring and who's just complaining. What it lacks isyour price list and your no-go zones. If you don't tell it "a five-page site on a NT$20,000 budget is something we can do," it has no baseline to judge against. If you don't tell it "anyone demanding a free demo up front goes straight in the bin," it will work very hard writing an enthusiastic reply to someone who is never going to pay.
The results in practice were clean. The first sweep covered 85 posts; the zero-scored ones included: freelance platforms promoting themselves, designers pitching their own services, interior design and renovation requests, airport transfers, Japanese stock market investing tips, an anime chat in Vietnamese, and one "looking for someone to build a portfolio site, not much budget, could we trade portfolio work?"
All blocked. Not one slipped through.
4. Four disguises
Next comes the part of that afternoon that took the longest and is most worth writing down.
For the scheduled browser to find the latest posts, it has to be logged in. That step held me up for nearly an hour, across four misdiagnoses — and every time, the broken thing was disguised as a different kind of broken.
Disguise one: my check was interrupting the user.
I wrote a login routine that checked the page state every three seconds to see whether login had completed. The problem: Threads login bounces over to Instagram for authentication and then back, and in the instant mid-redirect the page happened to satisfy my "logged in" condition, so the program navigated away to verify — verification failed, and it redirected back to the login page.
The symptom the user sees:endless bouncing between Threads and IG, never logging in.
It looked exactly like a problem on Meta's end. In reality my checking code was destroying the very thing it was checking. The fix was to make the entire polling loop passive: don't touch the page at all during login, just read the login credentials the browser has stored.
Disguise two: it logged in, but not into that.
Having switched to reading credentials, I wrote the condition as "is there a login credential named sessionid?" If yes, success.
Turns out Instagram's credential has the same name. The moment the user logged into IG, my program declared success nine seconds later and closed the window — while the Threads step hadn't even been clicked yet.
Logging into IG is not logging into Threads. Written out it sounds idiotic, but in code it's a sloppy string comparison.
Disguise three: the verification used a signal that fires on both success and failure.
I assumed fixing the first two settled it, because the program printed "✅ Login complete, search returns 10 posts."
The problem is —you can read posts without being logged in. Threads' search page returns results for logged-out visitors too; it just quietly ignores your "recent only" setting and hands you posts from 2024 and 2025. So "posts are readable" holds true in both the success and failure cases — it can't serve as evidence at all.
The reliable signal I eventually found was:check the timestamps on the returned results — how many of the first six are from the past seven days. That signal only appears when you're genuinely logged in.
This is the lesson I think is most worth keeping: a verification signal has to be something that appears *only* on success. A light that comes on for both success and failure isn't a light, it's decoration.
Disguise four: a disabled account looks identical to being logged out.
With the first three fixed, the program finally reported a successful login. But on an actual sweep, the search page came back blank — zero posts, zero login links.
I dumped the whole page's text and found:
We've disabled your account ⋯ After review, we found that your account still violates our Community Guidelines on account integrity ⋯ You cannot request another review of this decision.
At login I'd picked an idle old account, and that account had been disabled six months earlier. Threads handles disabled accounts by redirectingevery URLto the same notice page — home, search, profile, all of them. That page has no content, but it also has no "log in" button, so in my detection logic it counted as neither logged out nor logged in: just a blank nothing.
Switched to the right account and it cleared in thirty seconds.
What the four disguises have in common:every failure looks like a different failure. And the first three are a bit more embarrassing — it wasn't the feature that broke, it was the code I'd written to confirm the feature worked that was lying to me.
The most dangerous state for an automated system has never been broken. Broken stops, throws errors, gets someone yelling at you. What's dangerous is broken-that-looks-fine: it runs on schedule every day, prints green checkmarks, never misses a notification — it's just reading old posts from 2025.
So I deliberately made this thing loud. No posts found, logged out, account disabled, judging timed out, any uncaught exception — all of it gets pushed to Telegram in red, each clearly labeled as to which kind it is. No silent degradation, no passing off cached results as a real run.No notification means there really were no leads.
5. And one atypical failure
The first real run pulled 85 posts, then the whole round died.
The cause: I'd stuffed all 85 posts into a single judging request. The model's reply has a length limit, it got truncated mid-write, and the returned data format broke — not a wrong judgment, the entire batch was void.
Switched to batches of 20. And while I was at it, I added one thing: save the raw data to disk immediately after scraping. A sweep takes four or five minutes, so if anything downstream dies, I can re-run the judging straight from the saved file instead of bothering Threads again.
This "batch size" trap is really the same species as the four disguises above: something quietly failing to meet spec somewhere you weren't looking.
6. What came out, and the caveat the AI wrote itself
First full sweep: 85 posts, 1 qualifying.
That one was a company wanting to redesign its website, needing three languages, budget negotiable. The AI scored it 8 and wrote a draft whose angle was: "do the three languages each need their own structured data for search engines, or is it just an interface toggle over shared content? That affects where the quote lands."
Next to the draft there's a "risks" column I'd asked it to fill in. It wrote, unprompted:
They're asking for "collaboration experiences / word-of-mouth," not a quote; leading with pricing will read as an ad and won't address the experiential questions they raised about communication flow and after-sales response time.
That column stopped me for a second.
It wrote a decent draft, and then told me right next to it that the draft's angle might be wrong — because what the poster wanted was someone else's experience, not an estimate.
That sentence is the complete answer to why I didn't go full auto. If pressing a button had sent it, that reply would have become canned reply number thirty-six. Instead it becomes a prompt: this one needs a different approach, or maybe shouldn't be answered by me at all.
7. Four days later, settling the accounts
Everything above was written the day the radar went live. The rule of this series is to be clear about the cost, so four days later I came back to close the books. The conclusion first:It did every single thing I asked of it, and the business result is zero.
Four days, nine sweeps, a cumulative 638 unique posts read, yielding 3 leads worth replying to. Judging quality stayed clean — what it blocked was agency pitches, Japanese stock ads, fortune-telling musings, guesthouse promos, with no false kills; on days with no leads the top score was only 2, meaning there genuinely were no leads, not that the threshold was too strict. Up to this point, the radar hits its spec.
Then comes the ugly part.
Of the 3 leads, I replied to only 1. The highest-scoring one (the three-language site from the previous section, 8 points) still hasn't been answered four days on.
And for the one reply I actually did post, I pulled the numbers from the Threads dashboard:
3 views. 1 like. 0 responses.
Three people. Not three hundred — three.
The radar itself had already written the reason in the "risks" column:"Post is nearly 48 hours old and the requirement is entirely unspecified; others may have already jumped in on the comments."By the time I replied, that post had 29 comments and I was at the bottom. The prophecy came true, and the prophet was my own code.
Somewhere in there I also made a decision that looked very scientific. The day after launch, I pulled the time distribution of 35 open-call posts and found two peaks: 08:00–10:00 after work starts (29%) and 20:00–21:00 after dinner (23%), with almost nothing overnight or in the afternoon. So I changed "sweep every three hours" to "two rounds a day, each parked just after a peak" — resource-efficient, data-backed, sounds entirely correct.
Once the performance numbers came in, that decision became the prime suspect. Because in a comment thread,replying late is the same as not replying. The handful of canned replies I posted manually that same week were noticeably worse than the AI drafts, but because they went up early, they got 40 to 256 views — dozens of times more than the carefully written one. The difference wasn't the copy, it was the timing.
But there's an even more embarrassing comparison. The same week, my traffic-driving comments undermy own postsgot 5,681, 8,832, 9,101, and 9,249 views apiece.
Pitching under someone else's open call gets you single- to triple-digit exposure. Building your own content gets you four digits.
Here's the honest boundary:three leads over four days is too small a sample for me to claim "replying early works" — because although those manual replies got high exposure, they also produced 0 responses and 0 inquiries. The data so far proves exactly one thing:replying late definitely doesn't work.Whether replying early works is still unverified, and that's precisely what comes next.
The fifth disguise
The first four disguises were all about "broken disguised as a different kind of broken." Four days later I hit a fifth, running the other direction:not broken, disguised as useful.
I deliberately made this system loud — no posts found is loud, logged out is loud, judging timeout is loud. Anything breaking makes a sound; I got that part right.
What I didn't do was give "alive, and achieving nothing" a sound.
So for four days, all green: the program ran on time, posts were scraped, judging was correct, notifications hit Telegram on schedule. Every dashboard normal. And the number that actually mattered —how many people saw the replies I sent— nobody ever checked, because the system never volunteers it.
I only pulled it up when I sat down to write this reckoning.
This is probably automation's most expensive lesson, more expensive than the first four disguises combined:you'll design alerts for "broken," but almost nobody designs alerts for "busy doing nothing."Broken stops, shouts, gets handled; busy-doing-nothing clocks in on time, shows all-green metrics, burns your resources daily, and then nothing happens — and it can keep that up for months.
So there's a new rule now: when any automated system goes live, on top of "will it shout when it breaks," it has to answer a second question —"Is it working — which number says so? Who looks at that number? How often?"If you can't answer, you're not done.
Wrapping up: the real deliverable
One afternoon, one radar, up and running. Clean judging, loud alerts, clear about what went wrong. Four days later, settling the accounts: it did the finding-leads part; it did not do the turning-leads-into-business part.
But if you asked me what this exercise actually produced, it isn't the program.
It's that capability brief. It's the list that forced me to write down what we take, for how much, delivered in how many weeks, and what we absolutely won't touch.
Before that, all of it lived in my head as tacit understanding. I quoted by feel and turned down work I didn't want by instinct. It worked perfectly well — as long as I was the one deciding.
The moment you hand judgment to something else to execute, tacit has to become spec. And writing that spec forces you to face questions you've been quietly avoiding: do we actually take work at this price? Do we actually want this kind of client? Of all our case work, which piece genuinely speaks for us?
That's probably automation's most underrated side effect. You think you're teaching a machine to do a job; in fact you're being forced to articulate yourself.
That list now lives in version control, and it gets updated whenever we change pricing on the site. It matters more than the radar — if the radar breaks I just write another one, but rewriting that list means rethinking who we are.
And that four-day reckoning added a caveat to all of it:however clear the list, nothing guarantees anyone is listening.What separates finding leads from winning leads isn't judgment quality — it's when you show up and where you show up. On that stretch of road, automation has saved me far less time than I expected.