← Writing
August 30, 2026·4 min read

Zero for Twenty-Five

I built an agent to get my phone number off spam-call broker lists. Its first real run didn't clear a single one — and the failure was more useful than a clean win would've been.

I've been getting loan robocalls for weeks. Enough of them that I finally said out loud, mid-conversation with Claude about something else entirely: "I just want off." Off every list. Off the thing that keeps selling my number to whoever's dialing me at 6pm about a loan I never asked about.

Most robocalls are spoofed and untraceable, so chasing the calls themselves is a dead end. But the calls have to come from somewhere — lead-gen operations buy phone numbers in bulk from people-search data brokers, the BeenVerified and Spokeo and Whitepages of the world. There's no single kill switch, but there is a list: roughly 200 known brokers, about 28 of which let you opt out through a plain web form instead of requiring ID uploads or a phone call. I asked if we could build something like DeleteMe or Incogni, but local, and free.

One boundary got set immediately and held for the rest of the project: no CAPTCHA-solving, no routing around bot detection, full stop. Not a technical limitation — a rule. Some of those brokers gate their opt-out forms behind CAPTCHAs specifically to make automated removal harder, and building past that crosses from "automating a form I'm allowed to submit" into something else. So the agent only touches brokers that don't put one up.

The build itself was a Playwright agent driven by a local model — Ollama running on a home server, deliberately not hardcoded to CSS selectors, since broker sites redesign constantly and a selector-based scraper breaks every time one of them ships a new layout. The LLM looks at the page and decides what to fill in, the same way a person would. Debugging it was the usual grind: an SSH permission error, a model version mismatch, a StopIteration crash, then an agent that just hung on the first broker for ten minutes. That last one turned out to be a missing timeout on the LLM call, not a flaky model — it was waiting on a response that was never coming back quickly enough, and nothing was there to cut it off.

Once it ran, speed became the real constraint. Each decision took the local model close to a minute, and at fifteen steps per broker across twenty-some brokers, a full pass was going to take hours. The fix took more thought than the bug did: swap in a faster cloud model, but never let real personal data leave the machine to do it. So the pipeline resolves form fields against placeholder tokens — {{full_name}}, {{email}} — and only substitutes Mike's actual information locally, after the model has already decided which field is which. The cloud model reasons about structure; it never sees the data.

Then I ran it for real, against twenty-five brokers. Zero succeeded.

Ten hit Cloudflare's "just a moment" interstitial. Four turned out to have CAPTCHAs the broker list hadn't flagged. Nine burned through their step budget in loops, retrying the same failed action. Two were dead ends outright — one broker returned a flat 403 from its WAF, which is not a problem an agent fixes.

The Cloudflare wall was the real finding. It wasn't that any single site was hardened against this specific agent — it was that hitting twenty-five different domains from one home IP address in fifteen minutes looks exactly like a bot regardless of what the bot is doing. The fix wasn't cleverer automation, it was patience: a cron job running one broker every four hours instead of all of them at once, loop detection that bails after three repeated actions instead of exhausting the full step budget, and a 25-day recheck cycle since brokers re-list people every one to three months anyway.

It's running now, slowly, correctly, in the background. Not because it got smarter, but because it stopped trying to look like a person doing twenty-five things at once — which is the one thing no actual person would ever do.