The problem: research by hand does not scale.
For every account a rep opens the site, checks fit and size somewhere else, hunts for something worth saying, and tiers it. Then the next one. At a few minutes each, a list of fifteen hundred accounts is weeks of selling time spent before anyone knows which accounts were worth it.
It runs overnight.
Give it company names and domains. It applies the team's written qualification protocol to every account, researches each one on the web, and appends a row to the workbook as it goes. By morning the list is done.
Every account gets the same research.
A rep has time to research the few accounts that look promising. The agent runs the same pattern on all of them: several public sources, reconciled into the evidence the protocol asks for, and a reason to call.
The list comes back ranked.
The workbook is sorted by priority, with the bucket, the evidence, and a call note on every row. A rep opens it and starts at the top.
The winA fully prospected market, and the context to tailor outreach account by account.
The market becomes a number.
Before the run, every row in the list counted as addressable. After it, the team knows how many accounts are actually workable, and can plan coverage, quotas, and hiring against that number instead of the row count.
How it's built
The same engine has run six markets. Change the qualification rules and the research sources and it prospects a different industry without a rebuild.
| Language | Python. |
|---|---|
| Input and output | pandas reads the list, openpyxl writes the workbook. |
| Concurrency | asyncio, throttled to three accounts at a time by a semaphore, under the API rate limit on purpose. |
| Research | An LLM with a web search tool. It chooses its own searches within a hard cap of six per account. |
Decisions
- Idempotent by domain. Completed domains are read from the workbook at startup and skipped, so a run is safe across kills, restarts, and appended input.
- Crash-safe writes. The workbook is opened, appended, and saved on every row. Deliberately I/O-heavy: a crash at any moment leaves a valid file and costs one account.
- Defensive parsing. JSON is pulled from the model's response with a brace-matching regex rather than trusting clean output. A missing priority is derived from a fixed bucket-to-priority map.
- Failures logged, not retried. Failed rows go to a failures file and are reprocessed on the next run, because they never reached the output.