Story 3 · Finding clients
Teaching my site to find its own prospects
My admin page now has a button called Find prospects. One press searches the web for companies that may need one of my services, checks what it finds in code, and lists each one for me to accept or dismiss. It remembers what it searched and what came of each find, so the next press goes further and does more of what worked. Nothing it finds is sent to anyone until I say so.
Forty-seven by hand
My first outreach list was 47 agencies and recruiters, researched by hand: a posting or a page that showed they might need what I build, the address they publish, an angle for the email, and what not to assume about them. I imported that list into my admin page, where each one gets an email from my own Gmail when I press Send and confirm.
The research was the slow part, and most of it was the same every time: search, read the posting, find the contact page, check the address is really there, write two sentences on why I'm writing. My site already knows my services and my profile. So I wanted the next names to come from a button with nothing to choose, and my time to go on the part a machine can't judge: whether to write at all.
What one press does
A press starts a run. The site builds a brief from its records, then hands it to Claude (Opus 5.5) with four tools and a budget. Claude searches, reads pages and reports each candidate. The site checks every candidate before it keeps it. I decide on each one, and what happens after that goes back into the records the next brief is built from.
flowchart TB
R[("The site's records")] --> B["A brief: my offers, the plan, what was searched, what worked"]
B --> L
subgraph L["The agent loop"]
direction LR
S["web search"] ~~~ F["web fetch"] ~~~ C["recall"] ~~~ A["add_find"]
end
L --> G{{"Checks in code"}}
G --> N["Finds"]
N --> J{"Me: accept or dismiss"}
J -->|accept| P["A drafted prospect"]
J -->|dismiss, with a reason| R2
P --> SE["Send, when I confirm"] --> RE["Replies"] --> O["Outcomes"] --> R2[("The records again, for the next brief")]
The searching is a small deep research agent: a model that searches and reads, over and over, until its budget is spent, rather than answering from one search. It sweeps four lanes, one for each kind of work on my home page: the starter assistant, the eval harness, the custom build and accessibility. For each it looks for job postings (contract work first), requests for proposals, agency pages advertising the work, and announcements from the last 90 days, US companies first.
Two of the tools are server-side tools, which means Anthropic runs them, not my site: web search and web fetch. My site never searches the web itself. The other two are mine. recall asks my records a question, and add_find reports a candidate. Both are tool use with strict JSON schemas, where the model calls a function I defined and its arguments must match the shape I declared, so a find always arrives with its company, domain, the posting's title and address, the lane, a summary, an angle and a suggested paragraph for the email.
Retrieval before and during the run
The run starts from what the records say, so it does not start from nothing. Before the first search, the brief carries my four offers in a sentence each, the headline and summary from my profile, the plan for this run, what earlier runs searched in each lane, and what came of their finds. It is built fresh each run and kept small on purpose: the last 8 searches in each lane, at most 40 domains examined, the 10 best searches, the 5 worst and the last 10 reasons I dismissed a find.
During the run the model can ask for more with recall: give it a company, a domain, a sector or a few words, and it gets back up to 20 matching rows from my records, newest first, each with what became of it. Retrieving before the run and again on demand, through a tool the model decides when to call, is what people call agentic RAG. The one-shot kind retrieves once, up front, and hopes it fetched the right things. My records are a few hundred rows a year, so a plain text match in the database is enough; the shape would not change if they ever needed a search index.
sequenceDiagram
participant J as Me
participant S as My site
participant C as Claude API
participant W as A company's site
J->>S: Find prospects
S->>S: Brief and plan from the records
S->>C: The brief, four tools, the caps
loop Until the budget is spent
C->>C: Web search and web fetch, run by Anthropic
C-->>S: Results to record, then recall or add_find
S->>W: Read the page the address is on
W-->>S: The page
S-->>C: Recorded or refused, verified or not
end
S-->>J: Each step as it happens, then Done
Every find carries its sources: the posting's address, and the page its email address was published on. That is grounding with citations: every claim tied to a page anyone can open. It is the part I check first.
Paging deeper without a page number
A web search has no "page 2" the next day. Results move, and the same words find the same companies. So the site keeps a ledger: every search the model ran, with its words, the domains of the results it got back, and how many finds it led to. It reads them from the search results Anthropic sends back, including searches the model ran inside its own filtering code, which come back nested and are easy to miss.
Each search is put in a lane by its words. "WCAG" or "VPAT" is accessibility, "golden set" or "hallucination" is the eval harness, and a search that matches no lane is counted as general. A find is credited to the most recent search of its run whose results named its domain. A lane's depth is never stored, only counted: how many different searches it has had and how many domains they turned up. The next brief lists them and tells the model to go past them, with new angles, narrower sectors, other job boards and other regions.
Learning from what came of it
A find's outcome is not written down twice. It is read from what became of it: whether I accepted or dismissed it, and, for one I accepted, whether the email went out, whether they replied (a reply in Gmail or a message through my site), whether it was won, and whether it bounced. Each search gets a score from the finds it led to: one point for each accepted, three for each reply, ten for each win, minus one for each dismissal and two for each bounce.
That is a feedback loop, and the ledger with its outcomes is the agent's episodic memory: not facts about the world, but what happened the last times it tried. The most direct lesson is the reason I give when I dismiss a find. "An in-house team of forty" says more than any score, so the last ten reasons go into every brief word for word.
The score also sets the next run's budget, which the brief gives the model lane by lane. Every lane gets 2 of the 16 searches whatever happened, and the other 8 are shared out in proportion to each lane's score over the last 90 days, equally while no lane has scored. That is an explore/exploit split: the floor keeps every lane explored, and the share sends more of the budget where replies came from.
flowchart LR O["What came of each find"] --> S["A score per lane"] S --> P["8 searches shared by score"] F["2 searches per lane, always"] --> B["16 searches a run"] P --> B
What it is not allowed to do
The model is good at searching and quick to sound sure. So the rules that matter are checks in code, not instructions in the brief. These are the guardrails, the checks a find must pass before the site keeps it:
- Nothing sends itself. A find is a candidate on a list. Accepting it makes a drafted prospect; the email goes only when I press Send, read it as it will go, and confirm. That is human in the loop: every find waits for my accept or dismiss.
- No address is ever guessed. An address must be on the company's own domain and come with the page it was published on. A free mail domain is refused.
- The site checks the address itself. It reads that page on its own (web pages only, 10 seconds, 1 MB, text only) and marks the address verified only if it finds it there, written out or hidden as "name (at) company". Otherwise the find says unverified and asks me to open the source.
- A company is found once. Its domain is unique in the records, and a domain or address that is already a prospect or a find, even one I dismissed, is refused. Reporting the same company again changes nothing, which makes each find idempotent, and the worst a repeated search costs is one search.
- Caps in code. A run gets 16 searches, 24 page reads and 15 finds, and the site starts at most 10 runs a day, one at a time. A run still going after 15 minutes is marked failed so it cannot block the next one.
- Owners only. Every route answers as a missing page to anyone else, a developer who works for me included.
What it costs
Anthropic charges $10 per 1,000 searches on top of the model's tokens, so a run's 16 searches cost at most $0.16. The tokens of reading and writing are the larger part. Every run records its own usage, searches, pages read, tokens in and tokens out, with a log of each step, and the admin page shows them beside the run. That observability is how I'll know what a find costs, rather than guessing.
This story is about the build, not the results: it went live before the first real run. When there are runs worth reporting, their numbers will come from an export of the runs, as every number in these stories does.
What this means for your project
The shape is not about prospecting. An agent that researches under a budget, grounded in a company's own records, checked in code, verified independently and decided by a person is what a custom build is, whether the records are tickets, contracts or a product catalogue: AI built on your company's data. The useful part is not the model, which anyone can call, but the records it learns from, the checks it cannot talk its way past, and knowing what each run cost.