> Enrich.chat gives AI agents verified work emails and mobiles. To use it from an agent, read https://enrich.chat/agents.md . This is the Markdown version of https://enrich.chat/blog/ai-agent-for-lead-generation/

# How to build an AI agent for lead generation: tools, guardrails, a spend cap and a person who reviews

A build guide for the outbound kind: a model, two data tools and a guard that paces, stops and records. The code was run against a stand-in server on October 16, 2026.

By Enrich.chat Editorial. Updated October 16, 2026.

**Short answer:** To build an AI agent for lead generation, give a model a written goal, a contact data tool and limits it cannot talk its way past. [OpenAI's agent guide](https://cdn.openai.com/business-guides-and-resources/a-practical-guide-to-building-agents.pdf) names three parts, a model, tools and instructions, and says high-risk actions need human oversight. Pace the calls, stop on a 402, honor not found, record the source and date, and review the list before anything is sent.

## Key takeaways

- "AI agent for lead generation" means two products: a website chat agent that collects visitor details, and an outbound agent that searches for people and looks up their contacts. This guide builds the outbound one.
- The loop has six parts: goal, ICP, search, enrichment, status handling and output. The model chooses who to look up. Code decides whether a call is allowed. A person approves the list.
- Rules in a prompt are requests. Rules in code are limits. A short guard around the API calls can pace requests, stop on a 402, wait on a 429, drop guesses, enforce a credit cap and record the source and date.
- A fictional run of one 20-person search and 15 email lookups uses 220 credits and fits the Growth plan at $29 a month.
- Enrich.chat supplies the search and the work email with a status. It does not send email, update a CRM or offer a screen, so sending and records stay with you.

## What is an AI agent for lead generation, and what will you build?

An AI agent for lead generation is a model that works toward a goal by calling tools in a loop, such as a people search and a contact lookup, until it finishes or hits a limit. [OpenAI's guide to building agents](https://cdn.openai.com/business-guides-and-resources/a-practical-guide-to-building-agents.pdf) describes an agent as a system that independently completes tasks for you, and lists three core parts: a model, tools and instructions.

The phrase covers two different products. [Chatbase's build guide](https://www.chatbase.co/blog/lead-generation-chatbot) covers the first: a chat agent on your website that asks visitors for their details. Its steps stop at the chat window. They do not cover outbound search, contact data or spend limits. This page builds the second kind, an outbound agent that finds people who match your buyer profile, looks up their work emails and hands you a list.

Build a loop only when the steps need judgment. [Anthropic's engineers](https://www.anthropic.com/engineering/building-effective-agents) separate workflows, where code fixes the path, from agents, where the model picks its own steps, and they advise starting with the simplest thing that works. If every row gets the same lookups, a fixed workflow is easier to check: see the [n8n lead enrichment guide](https://enrich.chat/blog/n8n-lead-enrichment/). Choose an agent when the model must decide who fits the profile or what to do with an odd result. For the whole outbound motion, with drafts and sending, read [how to build an AI SDR](https://enrich.chat/blog/how-to-build-an-ai-sdr/). This page is the data and safety layer underneath it.

## What does the lead generation agent loop look like?

The loop has six parts: a goal, an ICP, a search, an enrichment step, status handling and an output. The model decides who to look up next. Your code decides whether a call is allowed. You decide what gets sent.

**Figure 1: The lead generation agent loop and who controls each part.** A flow diagram. A brief with the goal, ICP, skip list and credit cap goes to the model. The model asks a guard, which is your code, to make each call to Enrich.chat. The guard paces calls, waits on a 429, stops on a 402, strips guesses and stops at the credit cap. Every answer goes into a ledger with its source, date and credits_used. The ledger feeds a hand-off list with three groups, Review, Hold and Skip, and a person reviews it. Sending and CRM updates sit outside the build.

*The model chooses the next lookup, your guard decides whether the call is allowed, and a person reviews the list. Orange marks the guard and the human gate. Editorial diagram.*

| Part | What it holds | Who does it |
| --- | --- | --- |
| Goal | One run, one list: 20 people and a credit cap | You write it in the brief |
| ICP | Titles, industry, country, headcount and a skip list | You write it, and search filters apply it |
| Search | `search_people` returns people with LinkedIn URLs | The model picks filters, the tool answers |
| Enrichment | `find_person_contact` adds a work email | The model picks who, the guard makes the call |
| Status handling | verified to Review, catch_all and unknown to Hold, found false to Skip | Rules in code |
| Output | Ledger rows with source and date, then a list you read | The guard writes, you review |

Two details save credits and embarrassment. First, a search result gives a company name, not a website, and the Enrich.chat person lookup does not accept a company name alone. The agent must never turn a name into a domain from memory. It uses the LinkedIn URL from the search result instead. Second, the agent should do the free step before the paid one. Dropping a poor fit by title costs nothing. A lookup can cost 10 credits.

Once the loop works, widen it one piece at a time. Add a trigger from [sales signals](https://enrich.chat/blog/sales-signals/), a buying-group search from [MEDDIC](https://enrich.chat/blog/meddic/), or a pre-call score from [lead qualification](https://enrich.chat/blog/lead-qualification/). To turn your [ideal customer profile](https://enrich.chat/blog/ideal-customer-profile/) into search filters, write it as a title, an industry, a country and a headcount range.

## What should the agent's instructions say?

Instructions should name the goal, the order of tools, the exit conditions and what to do with each status. [OpenAI's guide](https://cdn.openai.com/business-guides-and-resources/a-practical-guide-to-building-agents.pdf) advises breaking dense tasks into smaller steps, tying each step to a specific action, and covering edge cases with branches, such as an alternative step when information is missing. The block below does that for lead generation. Paste it into a Claude or ChatGPT project, or use it as the system prompt of your own loop, and replace the bracketed values.

```text
You are a lead research agent. Your goal is a reviewed list of [20] people who fit the ICP below. You never send, draft or post anything.
```

Each line carries a rule. Search comes first and the title filter second because the filter is free. The line about text inside a tool result matters too: [Claude Code's MCP docs](https://code.claude.com/docs/en/mcp) warn that servers which fetch outside content can expose you to prompt injection, and a job title is text someone else typed.

Know what this block is. In a chat app it is a request. The model usually follows it, and nothing forces it to. The next sections turn the rules that cost money or trust into code.

## How do you give the agent Enrich.chat as its data tool?

Add the Enrich.chat MCP server, or define two tools over its REST API. Both reach the same endpoints. Every endpoint is a plain GET, and the free tier needs no key.

**MCP.** The server is at `https://enrich.chat/mcp` (Streamable HTTP, no key). In Claude Code, `claude mcp add --transport http enrich https://enrich.chat/mcp` is the form [its docs](https://code.claude.com/docs/en/mcp) show for remote servers. The [install page](https://enrich.chat/install/) has the steps for each app, and [our AI SDR guide](https://enrich.chat/blog/how-to-build-an-ai-sdr/) walks through Claude and ChatGPT. The server lists eight tools, each marked read-only in its listing. The agent needs two or three of them.

| MCP tool | REST call | Give it to a lead agent? | Credits |
| --- | --- | --- | --- |
| `search_people` | `GET /v1/search` | Yes: up to 25 people a call | 5 per person returned |
| `find_person_contact` | `GET /v1/person` | Yes, with fields=email | 10 per work email found, 0 when found is false |
| `company_people` | `GET /v1/company/people` | Yes, when your list already has company websites | 5 per person returned |
| `company_tech_stack` | `GET /v1/company/tech` | Optional: needs a company website | 5 per company found |
| `company_profile` | `GET /v1/company` | Optional: needs a company website | 5 per profile found |
| `find_work_email` | `GET /v1/email` | Rarely: a name and website only, and a guess is not data | 10 per work email found |
| `find_mobile_number` | `GET /v1/mobile` | No, not by default | 250 per mobile found |
| `check_free_quota` | `GET /v1/usage` | Yes, at the start of a free-tier run | Does not use a lookup |

In Claude Code, [permission rules](https://code.claude.com/docs/en/permissions) name MCP tools as `mcp__enrich__search_people`, so you can allow the lookups you want and leave the mobile tool out. Credits and the person lookup input rules come from the [Enrich.chat agents file](https://enrich.chat/agents.md): the person lookup takes a LinkedIn URL, a work email, or a name plus a company website. Personal mailboxes such as Gmail are not looked up.

**REST.** If you write your own loop, define the tools yourself. [Claude's tool use format](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) needs a `name`, a `description` and an `input_schema`. Your code runs each `tool_use` block and sends back a `tool_result`. Save the three code blocks in this guide, in order, as one file.

```
# lead_agent.py, part 1 of 3: the two tools the model sees
def prop(kind, note):
    return {"type": kind, "description": note}


TOOLS = [
    {
        "name": "search_people",
        "description": (
            "Find people by role, industry, country and company size. "
            "Returns name, title, company name and linkedin_url, not "
            "email. A company name is not a website: never turn one into "
            "a domain. A search that is too broad returns found false."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "q": prop("string", "Who to find, in plain English"),
                "title": prop("string", "For example: director of ops"),
                "industry": prop("string", "For example: logistics"),
                "country": prop("string", "For example: United States"),
                "employees_min": prop("integer", "Smallest headcount"),
                "employees_max": prop("integer", "Largest headcount"),
                "limit": {"type": "integer", "minimum": 1, "maximum": 25},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "find_person_contact",
        "description": (
            "Find one person's work email and its status (verified, "
            "catch_all, unknown) from a linkedin_url that search_people "
            "returned. found false means no email: write not found. "
            "Never guess an address."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "linkedin_url": prop("string", "From search_people"),
            },
            "required": ["linkedin_url"],
            "additionalProperties": False,
        },
    },
]
```

Three choices in that code are deliberate. First, the descriptions are prompts. [Anthropic](https://www.anthropic.com/engineering/building-effective-agents) says tool definitions deserve as much prompt engineering as the main prompt, and its [tool-writing guide](https://www.anthropic.com/engineering/writing-tools-for-agents) suggests describing a tool as you would to a new teammate. Second, the person tool exposes only `linkedin_url`, so a model cannot ask for a mobile. Third, `linkedin_url` is required. [Claude's docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) warn that when a required input is missing, a model may infer a value instead of asking. The guard therefore refuses a call without it.

## Which guardrails keep the agent honest and inside its budget?

Nine rules cover most of the risk: pace, misses, guesses, records, a 402, a 429, a spend cap, opt-outs and a human review. Each has a home, and the ones that cost money or trust belong in code.

| Guardrail | The rule | Where it lives |
| --- | --- | --- |
| Pace | About one request a second, one call at a time | Code: a sleep before every call |
| Not found | `found: false` is a result: write not found, do not retry | Code: stored once, sent to Skip |
| Never guess | Report only what a tool returned, and ignore `guess` | Code: the field is removed |
| Record | Source and date on every row | Code: written to the ledger |
| 402 | Stop, do not retry, show `upgrade_url`, list unfinished rows | Code: a stop the model cannot undo |
| 429 | Wait `retry_after` seconds, then repeat the same call | Code: sleep and retry |
| Spend cap | Stop when credits used would pass the cap | Code: checked before each call |
| Opt-outs | Skip anyone on your do-not-contact list | Code: no request is made |
| Human review | Nothing is sent until a person approves | Design: the agent has no send tool |

These match what the guides advise. [OpenAI's guide](https://cdn.openai.com/business-guides-and-resources/a-practical-guide-to-building-agents.pdf) says a run is a loop with exit conditions such as errors or a maximum number of turns, and that two triggers should hand control to a person: failure thresholds and high-risk actions. [The MCP specification](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) says there should always be a human in the loop who can deny tool calls, and that clients should log tool use. [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585) defines 429 Too Many Requests and says a response may carry a Retry-After header.

The Enrich.chat statuses are labels. `verified`, `catch_all` and `unknown` describe a work email, and none of them is a delivery guarantee, so treat every contact as a strong lead. The [email verification API guide](https://enrich.chat/blog/email-verification-api/) shows how to handle such labels and error codes across vendors.

## How do you enforce the guardrails in code, and how do you test them?

Put one guard between the model and every API call. It paces requests, checks the cap, handles each error code, removes guesses and writes the ledger. The model never touches the network.

```
# lead_agent.py, part 2 of 3: the guard that wraps every call
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import date

BASE = os.environ.get("ENRICH_BASE", "https://enrich.chat/v1")
KEY = os.environ.get("ENRICH_KEY", "")  # optional: the free tier needs none
EMAIL, PROFILE = 10, 5  # credits used when found
SEARCH_KEYS = ("q", "title", "industry", "country",
               "employees_min", "employees_max")


class Stop(Exception):
    """End the run: a 402, a bad key, repeated failures or the cap."""


class Guard:
    def __init__(self, cap, pace=1.0, ledger="ledger.jsonl"):
        self.cap, self.pace, self.ledger = cap, pace, ledger
        self.spent, self.last, self.halt, self.seen = 0, 0.0, None, {}
        if os.path.exists(ledger):  # resume: reuse paid-for answers
            for line in open(ledger):
                row = json.loads(line)
                self.seen[row["key"]] = row["body"]

    def stop(self, why):
        self.halt = why
        raise Stop(why)

    def get(self, key):
        req = urllib.request.Request(BASE + key)
        if KEY:
            req.add_header("Authorization", "Bearer " + KEY)
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                return r.status, json.load(r)
        except urllib.error.HTTPError as e:
            return e.code, json.load(e)

    def call(self, path, params, worst):
        key = path + "?" + urllib.parse.urlencode(sorted(params.items()))
        if self.halt:
            raise Stop(self.halt)
        if key in self.seen:  # never pay twice for the same lookup
            return self.seen[key]
        if self.spent + worst > self.cap:
            self.stop("spend cap: %d of %d credits used"
                      % (self.spent, self.cap))
        for attempt in (1, 2, 3):
            pause = self.last + self.pace - time.time()
            time.sleep(max(0.0, pause))
            self.last = time.time()
            status, body = self.get(key)
            if status == 429 or (status == 503 and attempt == 1):
                time.sleep(float(body.get("retry_after", 5)))  # then resume
                continue
            break
        if status in (401, 402, 429, 503):
            self.stop("%s %s %s" % (status, body.get("error"),
                                    body.get("upgrade_url", "")))
        if status != 200:  # 400: the model fixes it, nothing is retried
            return {"error": body.get("error"),
                    "message": body.get("message")}
        body.pop("guess", None)  # a guess is not data
        body["source"] = "GET /v1" + path
        body["fetched"] = date.today().isoformat()
        self.spent += body.get("credits_used", 0)
        self.seen[key] = body
        with open(self.ledger, "a") as f:
            f.write(json.dumps({"key": key, "body": body}) + "\n")
        return body


def dispatch(guard, name, args, skip=()):
    if name == "search_people":
        limit = min(int(args.get("limit", 10)), 25)
        params = dict((k, args[k]) for k in SEARCH_KEYS if k in args)
        params["limit"] = limit
        return guard.call("/search", params, worst=limit * PROFILE)
    if name == "find_person_contact":
        url = args.get("linkedin_url")
        if not url:  # a missing input is never filled in by a guess
            return {"error": "bad_request",
                    "message": "linkedin_url is required. Do not guess."}
        if url in skip:  # your do-not-contact list
            return {"found": False, "skipped": "do-not-contact list"}
        # Ask for the email only. A mobile would cost far more.
        params = {"linkedin_url": url, "fields": "email"}
        return guard.call("/person", params, worst=EMAIL)
    return {"error": "unknown tool: " + name}
```

The guard keeps the rules that matter in plain code. It sets `fields=email` itself. It checks the worst case of each call against the cap before it sends the request, so a run can end under the cap but never over it. It waits `retry_after` seconds on a 429 and tries the same call again, and it retries a 503 once. On a 402 or a 401 it stops and stays stopped. If you have a key, set `ENRICH_KEY` and the guard sends it as a Bearer header. The free tier needs none.

```
# lead_agent.py, part 3 of 3: the loop and the hand-off
STOPPED = ". Make no more tool calls. List the rows you did not finish."


def run(ask, guard, brief, skip=(), max_turns=40):
    """ask(messages, tools) is your model call. It returns content
    blocks as dicts (type, id, name, input, text)."""
    messages = [{"role": "user", "content": brief}]
    why = "finished"
    for _ in range(max_turns):
        content = ask(messages, TOOLS)
        messages.append({"role": "assistant", "content": content})
        calls = [b for b in content if b["type"] == "tool_use"]
        if not calls:
            break
        results = []
        for c in calls:  # one at a time, even if the model sent ten
            try:
                out = dispatch(guard, c["name"], c["input"], skip)
            except Stop as s:
                why = "stopped: %s" % s
                out = {"error": "stopped", "message": why + STOPPED}
            results.append({"type": "tool_result",
                            "tool_use_id": c["id"],
                            "content": json.dumps(out),
                            "is_error": "error" in out})
        messages.append({"role": "user", "content": results})
        if guard.halt:  # one last turn, with no tools, for the hand-off
            messages.append({"role": "assistant",
                             "content": ask(messages, [])})
            break
    else:
        why = "max_turns"
    return why, messages


def handoff(ledger):
    groups = {"review": [], "hold": [], "skip": []}
    names = {"verified": "review", "catch_all": "hold", "unknown": "hold"}
    for line in open(ledger):
        row = json.loads(line)
        if not row["key"].startswith("/person"):
            continue
        body = row["body"]
        email = body.get("work_email") or {}
        query = urllib.parse.parse_qs(row["key"].split("?")[1])
        groups[names.get(email.get("status"), "skip")].append({
            "name": body.get("name"), "title": body.get("title"),
            "email": email.get("value"), "status": email.get("status"),
            "linkedin_url": query["linkedin_url"][0],
            "source": body["source"], "fetched": body["fetched"],
        })
    return groups
```

The loop runs one call at a time even when the model sends ten in one turn, so the pace holds. When the guard stops, the loop sends the model one last turn without tools, so it can write the hand-off note. `handoff` builds the three groups from the ledger, not from what the model remembers. The `ask` function is the one piece you supply: your model call, returning content blocks as dictionaries.

The ledger is the record of the run, and the hand-off row is its unit. Each row holds the fields the tool returned, plus the source and the date the guard added.

**Figure 2: One row of the hand-off list, annotated.** A card shows one hand-off row for a fictional person: name Tomas Reyes, title Director of Operations, email tomas.reyes@example.com with the status verified, source GET /v1/person, fetched 2026-10-16 and credits_used 10. Notes on the right say the name, title and email come from the tool, the status is a label and not a delivery guarantee, the source and date are written by the guard, and credits_used is what the lookup used.

*A hand-off row for a fictional person, with field names that follow the Enrich.chat [agents file](https://enrich.chat/agents.md). The source and fetched date come from the guard, so every row can be traced.*

We ran this code against a stand-in server on localhost. The stand-in returns the response shapes in the Enrich.chat agents file for twenty fictional people on example.com, and can inject a 429, a 503, a 401 and a 402. A scripted function took the model's place, so the run checks the guard and not the model's judgment. It never touched the live API or a real person.

| The stand-in answered | The guard did |
| --- | --- |
| 16 answers, all 200, at the default pace | Sent requests about a second apart (15.1 s in all), used 220 credits and sorted 9 Review, 3 Hold and 3 Skip |
| A 429 with `retry_after` of 1 on the sixth request | Waited 1.01 s, sent the same request again and used the same 220 credits |
| A 503 on the third request | Retried once after `retry_after`, then finished the run |
| A 402 `free_limit_reached` after ten counted lookups | Stopped at the first 402, sent nothing after it, and the last turn listed 4 rows as not finished |
| A cap of 200 credits | Stopped at 200 with 3 rows left. A second run from the ledger with a cap of 250 sent only those 3 requests, and the two runs together used 220 |
| A model that asked for `fields=email,mobile,profile`, and a person on the do-not-contact list | Sent `fields=email` on every call, never asked for a mobile and made no request for the listed person |
| A `found: false` answer with a `guess` field | Removed the guess before the model could see it |
| A 401 `invalid_api_key` | Stopped after one request |
| A call with no `linkedin_url`, and a model that never stops calling tools | Refused the call with no request, and ended the looping run at `max_turns` |

## What does a full run look like, and what does it cost?

One run here is a single search of 20 people, a title check, and 15 email lookups. It uses 220 credits and 16 requests, so at least 15 seconds at one request a second. The people, companies and counts are invented to show the maths. They are not a forecast of real match rates.

| Step | Call | Result | Credits used |
| --- | --- | --- | --- |
| 1. Search | `search_people`, limit 20 | 20 people with LinkedIn URLs | 100 |
| 2. Title check | None, the model decides | 5 dropped, 15 kept | 0 |
| 3. Email lookups | `find_person_contact`, 15 times | 12 found: 9 verified, 2 catch_all, 1 unknown. 3 found false | 120 |
| 4. Hand-off | None, built from the ledger | 9 Review, 3 Hold, 3 Skip | 0 |
| Total | 16 requests | Cap was 250 | 220 |

**Figure 3: Credits used by one illustrative run, against its cap.** A bar chart of an illustrative run in credits. The search of 20 people uses 100 credits and the 12 email lookups that found an address use 120. The 3 lookups that found nothing use 0. The run total is 220 credits, under a cap of 250.

*Credits for one invented run at Enrich.chat's published credit prices. The cap is the worst case, with every lookup matching. Misses use nothing, so the run ends below it. See [pricing](https://enrich.chat/pricing/).*

Here is the person call for one fictional lead, with the response shaped like the one in the agents file. It is illustrative.

```
GET https://enrich.chat/v1/person
    ?linkedin_url=https://www.linkedin.com/in/demo-tomas-reyes
    &fields=email

{
  "found": true,
  "name": "Tomas Reyes",
  "title": "Director of Operations",
  "company": {"name": "Harborline Freight"},
  "linkedin_url": "https://www.linkedin.com/in/demo-tomas-reyes",
  "work_email": {"value": "tomas.reyes@example.com", "status": "verified"},
  "credits_used": 10,
  "free_lookups_remaining": 9
}
```

On the free tier, `credits_used` shows what a plan would use and nothing is charged. With a key, the response carries `credits_left` instead of `free_lookups_remaining`. The free tier gives 10 counted lookups a day, and the search is one of them, so this run would stop at a 402 after about nine matches. The guard hands back the unfinished rows, and a rerun with a key picks up where the ledger stops.

In your own run, read the Hold group before you decide anything. A `catch_all` status means the domain accepts every address, so the mailbox cannot be confirmed.

## How do you keep the cost under control?

Cap the credits in code, cap the turns, and make every call as small as it can be. Data credits and model tokens are separate bills, so cap both.

- **Set a credit cap from the worst case.** The run above has a worst case of 250 credits: the search plus 15 emails that all match. The guard checks that the next call's worst case fits under the cap before it sends the request. That is cautious. Three lookups found nothing and used 0, and the guard still reserved room for them.
- **Ask for the email only.** A work email costs 10 credits and a mobile 250. The tool definition leaves out `fields` and the guard always sends `fields=email`. [Anthropic](https://www.anthropic.com/engineering/building-effective-agents) calls this poka-yoke: change the arguments so that mistakes are harder to make.
- **Size the search.** Each person returned costs 5 credits, and a limit of 25 costs 125. Search by role plus industry plus size, and set `limit` to the number of people you will look up. Add `with_email=true` to return only people who come with a work email. The email in that list has status `unknown`, so still call the person lookup for the label.
- **Never pay twice.** The ledger returns a stored answer for a repeated lookup and lets a stopped run resume. Treat a ledger as one campaign's record and start a new one for the next campaign.
- **Cap the model side.** [Claude's docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) count the tool names, descriptions and schemas you send as input tokens, so two short tools cost less each turn than eight. Use `max_turns` in your own loop. In Claude Code, print mode has `--max-turns` and `--max-budget-usd`, and its [CLI reference](https://code.claude.com/docs/en/cli-reference) says the budget is a client-side estimate that can differ from your bill and can be passed, so leave headroom.

[Anthropic's engineers](https://www.anthropic.com/engineering/building-effective-agents) note that agents often trade cost and speed for better results. That is why a cap belongs in the first version.

Monthly, a weekly run of this size uses 880 credits. That fits the Growth plan at $29 a month, with unlimited seats and no annual contract. Credits renew with each monthly payment and unused credits do not roll over. See [pricing](https://enrich.chat/pricing/) for every plan.

## What can't this setup do, and when is another tool the better choice?

This setup finds people and gives you a work email with a status. It does not send, keep records or give you a screen. Enrich.chat is a data layer for your own agent, so these gaps are by design.

- **No sending.** The agent has no send tool, and Enrich.chat does not send email, sequence or dial. You send from your own tool after you read the list.
- **No CRM app.** There is no native CRM app. The ledger is a file, and a sheet or CRM needs your own code or a workflow, such as the one in the [n8n guide](https://enrich.chat/blog/n8n-lead-enrichment/).
- **No UI and no saved lists.** There is no dashboard, and on self-serve plans there is no bulk export. The agent holds the list. The free [lead list builder](https://enrich.chat/tools/lead-list-builder/) runs one search at a time.
- **No intent or funding data.** For who is in market, read [sales signals](https://enrich.chat/blog/sales-signals/) and [intent data](https://enrich.chat/blog/intent-data/).
- **Websites, not company names.** The person lookup needs a LinkedIn URL, a work email, or a name plus a company website. A search returns up to 25 people a call, and one that is too broad returns `found: false` with a hint.

Choose a hosted tool when you want one product that finds, writes, sends and syncs to your CRM. Our guides to [AI SDR tools](https://enrich.chat/blog/best-ai-sdr-tools/), [AI prospecting tools](https://enrich.chat/blog/ai-prospecting-tools/) and [AI sales agents](https://enrich.chat/blog/ai-sales-agents/) compare them. If you already work in Claude Code, [Claude Code for sales](https://enrich.chat/blog/claude-code-for-sales/) shows that setup. If the agent only needs to find addresses, [AI email finders](https://enrich.chat/blog/ai-email-finder/) compares the tools built for that one job.

You stay responsible for the law that applies to your outreach. The FTC says CAN-SPAM [makes no exception for business-to-business email](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business), and that you must honor an opt-out within 10 business days. Our guides to [AI lead generation](https://enrich.chat/blog/ai-lead-generation/) and the [TCPA](https://enrich.chat/blog/tcpa-b2b-cold-calling/) go further. This is general information, not legal advice. Anyone can ask Enrich.chat to remove their data at [enrich.chat/opt-out/](https://enrich.chat/opt-out/).

To run the first step with no code, copy the prompt on this page into an agent that can reach Enrich.chat. The [guide for AI agents](https://enrich.chat/agents/) lists every field, and the [person enrichment API](https://enrich.chat/api/person-enrichment/) and [people search API](https://enrich.chat/api/people-search/) pages cover each endpoint.

## Frequently asked questions

### What is an AI agent for lead generation?

It is a model that works toward a goal, such as a reviewed list of 20 buyers, by calling tools in a loop until it finishes or hits a limit. Two kinds exist: a website chat agent that collects visitor details, and an outbound agent that searches for people and looks up their contacts. This page builds the outbound kind with a [people search](https://enrich.chat/api/people-search/) tool and a work email lookup.

### How do I build an AI agent for lead generation?

Write a brief with the goal, ideal customer profile, skip list and credit cap. Give the model two tools, a people search and a contact lookup. Wrap every call in code that paces requests, stops on a 402, waits on a 429, drops guesses and records the source and date. Have the agent sort people into Review, Hold and Skip, and read the list yourself before anything is sent.

### Can I build a lead generation agent for free?

You can try the data side for free. Enrich.chat gives 10 lookups a day with no signup, and a search counts as one lookup however many people it returns. That is enough to test the loop on a few people. A run that looks up 15 people needs more, so the agent should stop on a 402 and hand back the unfinished rows. See [pricing](https://enrich.chat/pricing/) for the monthly plans.

### How do I stop an AI agent from making up emails?

Never let the model supply an address from memory. Tell it to get every email from a tool and to write not found when a lookup returns found false. Then enforce it in code: remove any guess field before the model sees it, and build the final list from stored tool results. Read the status on each email too, because catch_all means the domain accepts every address. See [how to handle statuses](https://enrich.chat/blog/email-verification-api/).

### How much does an AI lead generation agent cost to run?

There are two bills, the model and the data. The example run on this page, one search of 20 people and 15 email lookups with 12 found, uses 220 Enrich.chat credits, which fits the Growth plan at $29 a month. Running it weekly uses 880 credits a month. Model cost depends on your plan and the number of turns, so cap both.

### Should the agent send the emails itself?

Not at first. OpenAI's agent guide says sensitive or irreversible actions should trigger human oversight until confidence grows, and a sent email cannot be unsent. Give the agent no send tool, review its Review list, and send from your own tool. The [FTC's CAN-SPAM guide](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business) makes no exception for business-to-business email. This is general information, not legal advice.

## Sources

1. [Building effective agents](https://www.anthropic.com/engineering/building-effective-agents)
2. [A practical guide to building agents](https://cdn.openai.com/business-guides-and-resources/a-practical-guide-to-building-agents.pdf)
3. [Writing effective tools for agents, with agents](https://www.anthropic.com/engineering/writing-tools-for-agents)
4. [Tool use with Claude](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)
5. [CLI reference](https://code.claude.com/docs/en/cli-reference)
6. [Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp)
7. [Configure permissions](https://code.claude.com/docs/en/permissions)
8. [Model Context Protocol specification: Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
9. [RFC 6585: Additional HTTP Status Codes](https://www.rfc-editor.org/rfc/rfc6585)
10. [How to Build an AI Agent for Lead Generation](https://www.chatbase.co/blog/lead-generation-chatbot)
11. [CAN-SPAM Act: A Compliance Guide for Business](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business)

## Try this prompt

Run a capped lead research pass

```text
Use www.enrich.chat to find 20 directors and VPs of operations at US logistics companies with 200 to 1,000 employees. Drop anyone whose title does not fit. Look up each remaining person's work email one at a time, about one request a second. Put verified emails in Review, catch_all and unknown in Hold, and not found in Skip. Never guess an address. Stop on a 402 or when credits_used reaches 250, and list the people you did not finish. Keep the source and date for every row. Do not draft or send anything.
```

## Related

- [How to build an AI SDR with Claude or ChatGPT](https://enrich.chat/blog/how-to-build-an-ai-sdr/)
- [Best MCP servers for sales: sign-in, cost and tools](https://enrich.chat/blog/best-mcp-servers-for-sales/)
- [n8n lead enrichment: HTTP Request node, step by step](https://enrich.chat/blog/n8n-lead-enrichment/)
