Skip to content

Guides

The outbound system

Find, verify, organize, send, with a human on every wave. A working guide for a small business that wants a steady, lawful way to reach new customers by email: find prospects, verify a published contact for each one, organize them by how you got them, and send capped, plain-text messages that a person approves wave by wave.

About 40 min readWritten by Joseph Neese, Stratamize, Newnan GA

01 — Start here

Who this is for, and what you will have at the end

This is for the owner or office manager of a small business who wants new customers without buying a list, without burning the company domain, and without hoping a bulk tool is doing the right thing. It assumes you, or someone you trust, can open a terminal, or that you work with an AI coding agent that can.

The examples are deliberately ordinary: a landscaping company that wants property managers, a dental office that wants nearby employers for a benefits lunch-and-learn, a boutique that wants local stylists and event planners, and a B2B service firm (say, a bookkeeper) that wants newly formed companies in the county. The system is the same for all four. Only the lists and the words change.

When you reach the end you will have:

  • A dedicated sending identity that passes SPF, DKIM and DMARC, with an unsubscribe alias and plus-addressing.
  • A CRM (any one with an API) holding one lead record per prospect, with the field that matters most: where the contact came from.
  • A website form that lands hand-raisers in the CRM through a small server-side bridge, so your CRM key never reaches the browser.
  • A repeatable way to find a published business email for each prospect and have a second agent re-open the source and confirm it.
  • Your prospects organized by how you acquired them, with a one-page brief per group that says what you may send and how.
  • A plain-text sender that is dry-run by default, will not send without a human approval tied to the exact content, caps itself per day, spaces messages out, and stops a sequence the moment someone replies, bounces or opts out.
  • A Friday scoreboard that tells you where to put next week's hours.
9

phases, in the order we would build them again

~16

checks that must pass before each message leaves

6 in 10

typical share of candidates that become send-ready, verified rows

10

messages a day on day one of warm-up, rising over about two weeks

Time and cost

Roughly three to five weeks of part-time effort for one person working with an AI coding agent. The sender and its tests are the long pole; the identity, the form and the first lists are days, not weeks. Fixed costs are small: a domain (about $10–20 a year), one business mailbox (about $7–14 a month) and, if you self-host the CRM and the form bridge, a small server (about $6–24 a month). Those are approximate 2026 list prices; check them. Your AI subscription and your own time are the real spend.

Prerequisites

Accounts
A domain registrar; a business mail provider that lets a program send and read mail on one mailbox (an API, or at least authenticated IMAP to read and SMTP to send); a CRM with an API, or a self-hosted one; a task tracker with an API (we use a self-hosted one; many CRMs can double as it); a small Linux server or any host you control.
Tools
A terminal; Python 3 or a language you are comfortable in; a web server such as nginx; systemd; git; an AI coding agent that can run parallel sub-agents and web searches (Claude Code, for example), used for building the pieces and for the find-and-verify work.
People
One named approver. Every wave is approved by a person, by name, in the task tracker, and the approval task is assigned to them. In a small business that is usually the owner.
Decisions
Your business's postal address and a truthful sender name. A short list of things you will never do: no purchased lists, no cold text messages, no automated calls, no guessing email addresses.

Four rules the whole thing rests on

  1. Finders never send.Anything that produces a list hands it to the verify step. The right to send is attached to verification and consent posture, never to whoever found the row.
  2. Only a published business address, with a source URL.If the business did not print the address on its own website or in a public directory entry, you do not have it. No guessing, no brokers, no personal addresses.
  3. Organize by how you got the contact, not by industry.Consent and legal exposure follow the acquisition path. Industry decides the pitch; the path decides whether and how you may contact them.
  4. A person approves every wave, and the sender reads the approval back.“Go ahead” in a chat window is not an approval. An approval is a task, tied to the exact recipients and words, marked done by a human.

02 — The shape of it

The system at a glance

Read it left to right on a wide screen, top to bottom on a phone. Candidates come in from several finders, get verified, get sorted by how they came to exist, and only then reach a sender that a person has to unlock. What comes back from the mailbox feeds the next wave.

FindPhases 3 and 4

  • Website form (hand-raisers)
  • Public registries: state business filings, license rolls
  • Maps and places data
  • Hand research
  • Partners and referrers

Output: candidate CSVs that name their source. Nothing here can send.

VerifyPhase 5

  • Agent A finds a published business email on the business's own site or in a public directory entry
  • Agent B re-opens the source and tries to refute it
  • Survivors only; rejects kept with reasons

Output: a 15-column CSV with source_url, verified and still_operating on every row.

OrganizePhase 6

  • Six funnel categories, by acquisition path
  • One brief per category: consent posture, channel, do-nots, copy slots
  • Claims checklist and structure check

Output: one category at a time, with its brief, into the sender.

Approve and sendPhases 7 and 8

  • import, with the provenance guard
  • preview, then plan: one wave, one approval task, hashed to the content
  • A person marks the task done with the confirmation word
  • send: roughly sixteen checks, then reserve, then one plain-text message
  • Weekday window, warm-up cap, random spacing

Output: one logged attempt per message, under one approval.

LearnPhases 7 and 9

  • sync reads the mailbox
  • Reply: sequence stops; a person is told
  • Hard bounce: suppress and count; soft bounce: record only
  • Opt-out: honored at once; all but token-matched ones also open a task
  • Friday scoreboard by category and channel

Output: a growing suppression list and a decision about next week's hours.

Feedback: suppressions, replies and the scoreboard flow back into every later wave. A second sender, if you ever add one, must share the same suppression list.

Underneath everything: a dedicated sending identity (Phase 1), a CRM with an API plus a task tracker (Phase 2), one shared suppression list, and secrets that live in a secrets manager or an encrypted file, never in a repository or a chat transcript.

The crimson box is the only place a human is required. Everything to its left can be automated; nothing to its right runs without it.

03 — Do the work

The nine phases

Each phase has the same four parts: the steps, a checkpoint you can see with your own eyes before moving on, which templates below apply, and the pitfalls. Build them in this order. Domain and DNS first because propagation and warm-up are the slowest clocks in the system; the sender late, after there is somewhere lawful to send to.

Phase 1 of 9

Sending identity

Cold email goes out from a domain you can afford to warm up slowly and, if you get it wrong, retire. Never from the domain your invoices and customer replies depend on.

Steps

  1. Register a dedicated outreach domain that is obviously yours, for example a variation on your business name (your-outreach-domain.com). Point the bare domain at your real website so a curious recipient lands somewhere real.
  2. Create one mailbox on it in a real person's name, yours or the approver's. One is enough at the volumes in this guide, and warm-up is per mailbox and per domain.
  3. Publish SPF, DKIM and DMARC (records in the templates). SPF names the servers allowed to send for the domain; DKIM signs each message with a key only you hold; DMARC tells receivers what to do when those fail and where to send reports. Start DMARC at p=none (report only), watch the reports for two weeks, then move to p=quarantine (failures go to spam).
  4. Create two aliases. unsubscribe@your-outreach-domain.com delivers into the same mailbox, or into a shared one that somebody reads every working day. dmarc@your-outreach-domain.com receives the DMARC reports; they arrive as zipped XML that nobody reads by eye, so sign up for a free DMARC report viewer and either forward them to it or put the viewer's address in the record's rua= instead.
  5. Confirm plus-addressing on the alias: mail sent to unsubscribe+test@your-outreach-domain.com must land in the outreach mailbox with the +test tag still visible in the To header. The sender puts a per-recipient token after the plus on the unsubscribe alias, so an opt-out can be tied to the exact person it was sent to; replies are matched by the sender's address. If your provider drops the tag on aliases, create a second real mailbox named unsubscribe instead of an alias.
  6. Enable programmatic access on the mailbox with send plus read scopes. Send-only sounds safer; it is a trap (see pitfalls).
  7. Set a plain signature: name, business, website, postal address. No images, no banners.

Checkpoint

Send a test from the new mailbox to a personal address at a different provider. Open the received message's “Show original” (or “View source” / “Message headers”). You are looking for three results that all say pass: spf=pass, dkim=pass with d= equal to your outreach domain, and dmarc=pass. Then send one more test to unsubscribe+test@your-outreach-domain.com and confirm it lands in the outreach mailbox with +test intact in the To header.

If DKIM passes but d= shows your mail provider's own domain rather than yours, your domain's key is not yet aligned. Finish that step in the provider's admin console before any real send. Do not move to Phase 2 until all three pass with your domain in them.

Templates

  • DNS records with placeholders for SPF, DKIM and DMARC.
  • The footer block of the email skeleton doubles as the mailbox signature.

Pitfalls

  • Using the company domain “just for the first few”. One bad week puts your invoices in spam.
  • Choosing a send-only scope. A sender that cannot read its own bounces and opt-outs cannot honor them.
  • Setting DMARC to reject on day one, before the key is aligned, and bouncing your own test.
  • Several mailboxes “to go faster”. One identity is simpler to warm and simpler to defend.

Phase 2 of 9

A place for leads

Any CRM with an API will do, including a self-hosted one. What matters is the shape of the record and that your tools can read back what they wrote.

Steps

  1. Pick the system of record. It needs five calls: create a lead; add a timestamped note to a lead; read a lead's history back; create a task with a body; read a task's status and result text. If the CRM has no tasks, a separate task tracker with an API covers the last two (we use a self-hosted one).
  2. Define the lead record. The fields we would not go without: business name; contact name (only if published); email; email type (business-domain, a role address such as info@ or office@, or free-mail); phone (only if published); website; street address; category; source (which finder produced the row); source_url (where the email was published); acquired_on; funnel category (Phase 6); consent posture; status; notes.
  3. Learn the dedupe rule. Most CRMs dedupe on email, so a second form submission from the same address becomes a note on the existing lead rather than a new lead. Make sure your bridge and your reports expect that.
  4. Create one API credential with the narrowest scope the CRM offers for creating leads. Keep it in a secrets manager or an encrypted file. Never in a repository, never pasted into a chat.
  5. Wrap the five calls in a tiny command-line tool so a person or an agent can record work in one line. Everything that happens later is a lead, a note or a task in this system.

Checkpoint

From a terminal, create a test lead through the API, add a note to it, then read its history back and see both. Mark or delete the test lead. If the create call returns success but the read-back is empty, stop and fix that before building anything on top of it.

Templates

Pitfalls

  • Trusting a success response. A success status code is not a row. Read it back.
  • One shared admin token used by every tool. Use the narrowest scope available and rotate it when someone leaves.
  • Making industry the primary grouping. Store it, but group by source (Phase 6).

Phase 3 of 9

Capture hand-raisers

The cheapest lead is the one who wrote to you. A form on your website should become a CRM lead without the browser ever seeing your CRM key.

Steps

  1. Put one form on your site: name, business, email, phone (optional), what they want to talk about, a consent checkbox in plain words, and a hidden honeypot field that a person never sees and a bot fills in. A small script on the page fetches the bridge's config endpoint, then posts the fields as JSON; a plain HTML form post is not enough for the checks below.
  2. Do not post the form to the CRM from the browser. Post it to a small server-side bridge you run (a few hundred lines in any language) that holds the CRM credential.
  3. In the bridge, validate in this order: the request came from your own site (Origin and Sec-Fetch-Site, two headers the browser sets and a page cannot forge, plus a per-page CSRF token: a one-time value the config endpoint issues and the submit must echo back); the body is JSON and small; no unknown fields; each field has the right type and length and no control characters; consent is true; the honeypot is empty.
  4. Throttle: a small number of submissions per minute per client at the web server, and the same again inside the bridge. Answer 429 (too many requests) before writing anything.
  5. Write a receipt before calling the CRM: a random submission id (the form can mint it; the bridge validates it and uses it as the receipt reference), a hash of the payload, and a stage. No personal data in the receipt. Then create the lead, add a note carrying the submission id, and read the history back to confirm it landed.
  6. Return {ok, reference} to the browser, or an honest error. Show the reference on the thank-you message so the person can quote it if they call.
  7. At the web server, allow exactly the two paths the bridge needs (config and submit) and return 403 (forbidden) for everything else under /api/.
  8. Run the bridge as its own system user under systemd, with the credential handed to the process by the service manager rather than written into a config file.

A dental office's “which location” field and a landscaping company's “property type” field are the first personalization slots you get for free; carry them into the lead's notes.

Checkpoint

After pasting the two nginx snippets, run nginx -t and reload only if it passes. Submit the form from your phone. The lead appears in the CRM with source website form and the submission id in its notes. Then type a path you did not allow, such as /api/anything, into the browser address bar and confirm a 403. Then post the form twenty times quickly and confirm a 429; one line in a terminal does it:

for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" -X POST \
  https://your-real-website.example/api/YOUR_FORM/submit \
  -H "Content-Type: application/json" -d "{}"; done

All three, not just the first.

Templates

Pitfalls

  • The CRM key in the page's JavaScript. Anyone who opens the page can read it.
  • Testing only the paths you allowed. Test the ones you blocked.
  • No receipt. When the CRM is down you still want to know a submission happened and be able to replay it.
  • Repeat submissions from one address collapse into the existing lead (Phase 2 dedupe). Put the submission id in the note so nothing is lost.

Phase 4 of 9

Finders

Finders produce candidates. Each one names its source. None of them can send anything, and none of them produces an email address you are allowed to use yet.

Steps

  1. Public registries. Your state's business registry (new filings by county and formation date) and any license roll relevant to who you serve: contractor licenses for a B2B firm that serves builders, food-service licenses for a wholesale bakery. Read the registry's terms of use first. Use the registry's bulk download or API where one exists, and stop if the terms forbid automated access. Keep any automated collection polite: one page at a time, off-peak, nightly or weekly rather than continuously. Count rows per run; if a run returns far fewer than the last one, the page changed, not the world.
  2. Maps and places data. Good for categories, addresses and “still open” signals. If you pay for an API, put a hard spend cap in the code path that makes the call, not only in the vendor console, and store only the fields you need.
  3. Your own inbound (Phase 3). Hand-raisers are a finder too, and the best one.
  4. Hand research. The landscaping owner's list of forty property managers; the boutique's list of local stylists and event planners. Write them into the same CSV shape as everything else, with source = hand research.
  5. Partners and referrers. Firms that could send you customers: an accountant for the bookkeeper, a general contractor for the landscaper. These get one-to-one notes from a person, never a sequence.
  6. Every finder writes a candidate CSV with the first six columns of the 15-column CSV filled: id, source, name, category, address, website. Put the date you collected the rows in the file name. No email column is filled in yet. That is Phase 5's job.

Checkpoint

You have at least two candidate CSVs from two different finders. Every row says where it came from. You can point at each finder and say what it may never do. Nothing built in this phase has credentials for the mailbox.

Templates

  • The first six columns of the 15-column CSV are the candidate shape; the rest are filled by Phase 5.

Pitfalls

  • Registry pages change without notice and collectors fail silently. A run that returns zero rows for a month is the usual way you find out.
  • A spend cap in a vendor console is not a cap. The cap has to be in the code.
  • Registry rows rarely carry an email address. Finding one is a separate, verified step, never a lookup in a broker.
  • Licensed data providers restrict sharing; keep their rows out of shared sheets. A purchased list is not provenance.

Phase 5 of 9

Verify: the finder and the verifier

The only acceptable address is one the business published itself. Two agents look at every row: one finds, a second one with the opposite brief tries to refute. Only survivors move on.

Steps

  1. Split the candidate list into chunks of about a dozen businesses, small enough that one agent can hold the chunk and its sources in context.
  2. Agent A, the finder, per chunk: for each business, look only at the business's own website, its contact page, or a public directory entry. Return a published business email, its type (business-domain, role, or free-mail), the exact source_url, and whether the business appears to be still operating. It may not guess or construct an address, may not use data brokers or “email finder” services, and may not return a personal address. Respect each platform's terms: most maps and social platforms forbid automated collection, so anything from a maps or social listing is read by a person, not fetched by a script.
  3. Agent B, the verifier, in a fresh context, per chunk: open every source_url; confirm the page is about this exact business and the exact street address appears on it; confirm the email is shown as a contact on the page, not hidden in page code; set verified yes or no with a one-line reason. Its job is to refute.
  4. Merge: rows marked verified survive; rejects go to a second file with the verifier's reason. They are your training data for the next prompt. Dedupe on email.
  5. Write the 15-column CSV. Provenance means three things at once: verified is yes, source_url is an http(s) URL, and still_operating is yes. Anything else may be stored but is never planned or sent.
  6. Record the yield every run. For us, roughly six in ten candidates become send-ready rows. A sudden drop means a source changed or the finder started cutting corners. Investigate before you send.

Prompt outline

FINDER  (one chunk of about 12 businesses, fresh agent)
Given per row: name, street address, category, website if known.
Allowed sources: the business's own website or contact page; a public
  directory entry. Do not fetch maps or social listings.
Return per row: email, email_type (business-domain | role | free-mail | none),
  source_url, still_operating (yes | no | unknown), note.
Never: guess or construct an address; use a data broker or an email-finder
  tool; return a personal address.
Output: JSON matching the schema, one object per input row, no prose.

VERIFIER  (same chunk, fresh agent, opposite brief)
Given: the finder's rows.
For each row: open source_url; confirm the page is about this exact business
  and the exact street address appears on it; confirm the email is shown as
  a contact on the page, not only in page code; set verified (yes | no) and
  verify_note saying what you saw and where.
Your job is to refute. A row you cannot confirm is a no.
Output: JSON matching the schema.

Any AI coding agent that can fan out parallel sub-agents, run web searches and enforce a JSON schema on the output can play this. Fresh context for the verifier every time; an agent grading its own work agrees with itself.

Checkpoint

Pick five verified rows at random. Open each source_url yourself and find the street address and the email on the page. If any one fails, the verifier prompt is too loose: tighten it and rerun that chunk. Do not import a CSV you have not spot-checked by hand.

Templates

  • The 15-column CSV header and the meaning of each column.
  • The prompt outline above, adapted to your category.

Pitfalls

  • An address that appears only in a page's structured data, often a network-wide or agency contact for a chain, is not a published contact. Make the verifier say where on the page it saw the address.
  • Free-mail addresses may belong to a person. Label them and let the category brief decide.
  • Franchises share a head-office address. The exact-address rule catches most of this.
  • “Still operating” is a judgment from a web page. Recent reviews help; a phone call settles it.

Phase 6 of 9

Organize by acquisition funnel

Industry decides the pitch. How you got the contact decides whether and how you may contact them at all. Group by the second thing.

The six funnel categories with their consent posture, best channel, do-nots and personalization slots
CategoryHow they came to existConsent postureBest channelDo-notsCopy brief slots
Hand-raisersFilled in the form, called, walked in, repliedThey askedA personal reply within one business day, written by a personNo sequence, no automation on the first replywhat they asked for; when; the field they filled in
Hand-researchedThe owner's list; agent research against a clear ruleCold, but local and specificDoors and calls first for local businesses; email second, with a line that proves the researchNo bulk tone; no claim you cannot back; no more than three touchessomething specific from their site; why them and not the shop next door
Public registryNew filings; license rollsCold; no email on fileLetter or walk-in; email only after Phase 5 finds a published addressNo sequence to a brand-new owner in week one; regulated categories wait for counselformation month; county; what new owners usually need first
Scraped / places dataCollected from the open web or a maps APICold; must pass Phase 5Per the brief, after provenance is attachedNothing before verify; nothing to a free-mail address without a named personcategory; neighborhood; hours or a recent review you actually read
Third-party / inheritedA list someone gave or sold youFrozenNone until whoever built it writes down where each row came from and what each contact agreed toNever import into the sendernone
Partners / referrersFirms that could send you customersDirect B2B, person to personOne-to-one, by name, with a specific askNo sequences; no templates that read like onethe overlap between their customers and yours

Steps

  1. Tag every row with the finder that produced it. The finder decides the category; a person can re-categorize with a note saying why.
  2. Write a one-page brief per category (template): consent posture, best channel and the reasoning, a second channel, do-nots, a copy brief, and named personalization slots that say where each slot's content comes from.
  3. Write a claims checklist: phrases you will never write (guarantees, competitor names, “free”, “no risk”, anything you cannot prove on request), and the lines every message must carry (the solicitation line, your postal address, the opt-out line). The sender runs both halves on the rendered message, not on the template.
  4. Import into the sender one category at a time, so a wave's copy and do-nots match everyone in it.

The boutique that fills out a stylist's contact form is a hand-raiser the stylist may answer today. The same boutique found on a maps sweep is a cold contact that waits for verify. Same business, same industry, different permissions; that is why the category is the grouping.

Checkpoint

Hand any row to someone who was not involved. They can read its category and tell you, in one sentence, why you are allowed to contact that person and by which channel. If they hesitate, the brief is not finished.

Templates

Pitfalls

  • Inherited lists feel free. They are the most expensive thing you can send to.
  • Letting an industry template override a category's do-nots.
  • Linting the template instead of the rendered message. The slot is where the bad claim gets in.
  • A “parked” pile that is really a place to hide rows you want to send to later. Parked is a queue for decisions.

Phase 7 of 9

The sender

A plain-text sender you run yourself. Dry-run by default. It refuses to run unless a chain of checks passes and a person has approved the exact recipients and words.

Steps

  1. Build, or have your coding agent build, a command-line sender with a small database: contacts, waves, send attempts, the suppression list, daily counters. Commands: import, preview, plan, send, sync, status, suppress, resume. Dry-run is the default; live needs an explicit flag and everything below.
  2. Configure it. Two secrets, the mailbox credential from Phase 1 step 6 and the task-tracker credential, come from the secrets manager exactly as in Phase 2 step 4. Everything else lives in one configuration file that holds no secrets: the mailbox address and sender name; the unsubscribe alias; the sending window and its time zone; the postal address and the solicitation line for the footer; the stop-file path; the maximum age of an approval; the warm-up ceiling; the touch limit and the minimum gap between touches; the list of your own addresses the canary gate may send to; and canary_checked_on, left empty until Phase 8. Choose the numbers and write them down. As starting points rather than rules, we would pick a ceiling of a few dozen a day, an approval that expires in days rather than weeks, three touches at most, and at least a week between them.
  3. import reads the 15-column CSV. A row without provenance (verified yes, an http(s) source_url, still_operating yes) is stored as not eligible and can never be planned.
  4. preview renders every message with its slots filled and runs the claims checklist and the structure check on the rendered text.
  5. plan builds one wave: one category, N recipients, one subject, one body. It hashes recipients plus subject plus body and opens exactly one approval task in your task tracker (shape) whose body carries the preview, the counts and a confirmation word derived from the hash.
  6. Approve: a person reads the task and marks it done with the confirmation word in the result. Nothing else counts. Change one recipient or one word and the hash changes, the old approval is worthless, and you plan again. The task is assigned to the named approver from the prerequisites, and “result” means the task's free-text completion field, which some trackers call the comment or the resolution. Setting the task to blocked cancels the wave, even mid-run; that is the approver's emergency stop besides the stop file.
  7. send: before every single message the guards below all pass; then a reserved attempt row is written; only then is the mail API called. The reserved row is the idempotency record: proof that this message was attempted once, so an unknown outcome is never retried blindly. If the API's outcome is unknown, the row stays unsettled and nothing retries automatically.
  8. Pacing: a weekday daytime window in your time zone; a warm-up ramp keyed to days since the first live send (count the canary as that first send), starting at 10 a day and rising over about two weeks to the ceiling you configured; a random gap of a few minutes between messages, so one mailbox never sends in bursts and stays well inside what a single person could plausibly write. A follow-up is a new wave, planned and approved like the first; the touch limit and the minimum gap are what space it.
  9. The message: plain text; a real person's name in From; at most one link; no tracking pixels and no tracked links; a List-Unsubscribe mailto header carrying the per-recipient token; a visible opt-out line; the solicitation line; your postal address in the footer (skeleton).
  10. sync reads the mailbox. A live send runs it first and refuses to send if the mailbox could not be read; it runs again every few sends during the run. A reply stops that contact's sequence, is written as a note on the CRM lead so the hand-raiser rule takes over (a personal answer within one business day), and opens a task so a person sees it. A hard bounce suppresses the address and counts toward the circuit breaker; a soft or delayed notice is only recorded. An opt-out is detected by phrasing patterns (“remove me”, “take us off”, a subject beginning “unsubscribe”, a lone “stop” on its own line), or by any mail arriving at the unsubscribe alias. “Stop by the shop sometime” is a conversation, not an opt-out. Honoring never waits for a person: an opt-out matched by its token is honored silently; one recognized from phrasing or from the sender's business domain is honored at once and opens a task so a person can glance at it; one from an address that matches no contact suppresses that sender and, unless it is a consumer-mail domain, every contact at its business domain, and opens a task.
  11. Anyone who tells you to stop by any other channel, a phone call, a word at the counter, a reply to your company address: run suppress with the address and a reason the same day, and add a note on the CRM lead. The compliance box applies to every channel, not only the mailbox.
  12. Circuit breaker: a bounce spike over trailing sends, repeated provider errors in a row, or any single bounce or provider refusal that blames the sender (unsolicited mail, authentication, unusual volume) halts the mailbox until a person runs resume after reading why.
  13. When it stops: run status and read the halt reason and the last attempt rows. A bounce spike means the list is the problem: stop that category, re-verify or suppress it, and plan a fresh wave. Repeated provider errors mean the provider or the credential is the problem: wait, confirm the mailbox is reachable, then resume. After resume, the remaining recipients continue only if the approval is still valid and the content unchanged; if the approval aged out during the halt, plan again. A paused wave follows the same rule.
  14. Log every attempt: contact, wave, approval, timestamp, outcome, and the guard that stopped it if one did.

The checks before every message (roughly sixteen)

May this run send anything right now?

  1. Stop file absent. An operator halts everything by creating one file. No code change, no deploy.
  2. No open pause task in the task tracker. If the tracker cannot be read, fail closed (treat the unknown as a no): nothing runs.
  3. Mailbox not halted by the circuit breaker. Only resume clears it.
  4. No provider slow-down hold. If the mail provider recently answered “too many requests”, the hold must have expired.
  5. Approval still valid: task done, confirmation word present, younger than the maximum age. Re-checked on every message, not once at start.
  6. Inside the weekday window.
  7. Under today's cap on the warm-up ramp.

May this run send to this person?

  1. Not suppressed, by address or by exact domain (consumer-mail domains are never added as domain entries).
  2. Provenance present: verified, source_url, still_operating.
  3. Not already sent for this wave, and no unsettled prior attempt.
  4. Touch limit and minimum gap since the last touch to this contact; no more tries after repeated transport failures.
  5. Contact has not replied, bounced or opted out.
  6. Canary verified, or the recipient is you (Phase 8).
  7. Content unchanged since approval: the rendered message still hashes to what was approved. Drift stops the whole run.
  8. Rendered copy passes the claims checklist.
  9. Message structure valid: solicitation line, postal address, opt-out line all present.

Then reserve, then send. Cheap global checks run first so an incident, a remote pause or an off-hours run never reaches per-contact logic. Approval is checked before window and cap, so an unapproved wave never even learns its quota.

Checkpoint

Run send without the live flag against a wave of your own addresses and read the output: every guard listed, every one pass. Then edit one word of the body and run again: the content-drift guard must stop the whole run, not just one recipient. Then create the stop file and run again: nothing happens.

Your test suite must use a fake transport. Ours has a few hundred offline tests, and none of them can email anyone.

Pitfalls

  • Retrying when the API's outcome is unknown. The only honest idempotency is a reserved row written before the call.
  • Hashing only the recipient list. Hash the whole payload, and expire approvals.
  • Tests that can send. A fake transport is not optional.
  • Bare keyword opt-out detection. “Please remove the typo” is not an opt-out; “remove me” is. Match phrasing, and route the unmatched ones to a person.

Phase 8 of 9

Canary and first wave

One message to yourself first, with the headers read by a person. The gate that lets the sender address anyone else stays closed until that has happened and been written down.

Steps

  1. Leave the canary gate closed. Until an operator sets canary_checked_on in the configuration file, the sender will only address the own-address list from that file; every other recipient is left pending.
  2. Plan a wave of one: yourself, at a different provider. Approve it properly through the task tracker, confirmation word and all. No shortcuts for the test.
  3. Send live. Open the message and read the headers against the checklist below.
  4. Reply to it, then run sync: your contact row shows replied, the sequence is stopped, and a task appears. Plan a second wave of one to another address from your own-address list, approve and send it, reply “remove me” from that address and run sync: that contact becomes suppressed and a task appears. Finally, from a third address you can afford to lose (suppression is permanent; there is no undo command), send “unsubscribe” to the alias and run sync again: that sender is suppressed and a task appears, because the address matches no contact.
  5. Only now set canary_checked_on in the configuration file. The gate opens.
  6. First real wave: one category, the smallest sensible group (ten or fewer), hand-raisers or hand-researched first. Approve, send, and watch sync for two working days before planning the next.

First-wave checklist

What to check on the canary message before opening the gate
CheckWhat you should see
Authenticationspf=pass, dkim=pass with d= your outreach domain, dmarc=pass
Unsubscribe headerList-Unsubscribe present, mailto to your alias, with the per-recipient token after the plus
BodyPlain text; one link at most; your name; the solicitation line; the postal address; the visible opt-out line
PlacementInbox. The Promotions tab is normal for a new domain and is not a failure.
Reply pathYour reply lands in the outreach mailbox; sync marks the contact replied and opens a task
Opt-out pathA token-matched opt-out is honored silently; a phrased or unmatched one is honored at once and opens a task
LogOne attempt row with the wave, the approval and the outcome

Checkpoint

The canary date is recorded, the first wave went to ten or fewer people from one category, and two working days later nobody has bounced, nobody has opted out, and at least one person has written back something a human would write. If any of those is false, pause and read the log before wave two.

Templates

  • The first-wave checklist above.
  • The approval task body, used for the wave of one exactly as for a real wave.

Pitfalls

  • Opening the gate because the canary “looked fine” without reading the headers.
  • Sending the first wave to the coldest category because it is the biggest.
  • Planning wave two before sync has had two working days to show you what wave one did.

Phase 9 of 9

Measure

One table, every Friday. Move hours toward the category and channel that produce meetings, not the one that produces the most sends.

Steps

  1. Every Friday, fill in one scoreboard: rows are the funnel categories, columns are the channels (email, call, door, letter), and each cell holds three numbers: delivered first touches, genuine replies, meetings booked.
  2. Count a reply as genuine only if a person wrote it. Auto-replies, bounces and “unsubscribe” are not replies; the last two are suppressions.
  3. You have no open or click numbers, on purpose. There is no tracking in the message. Replies and meetings are the signal.
  4. Decide next week's hours from the table. The landscaper may find that doors beat email for property managers and email beats doors for HOA boards; the bookkeeper may find that letters to new filings outperform everything else. Follow the meetings.
  5. Review the claims checklist and the category briefs monthly against what people actually wrote back.
  6. Keep the suppression list growing and shared with everyone who sends for you. Every opt-out that reaches you by phone, in person or at the company address goes in with suppress the same day.

Checkpoint

Four Fridays in a row with the same columns, filled in by the same person, and at least one decision in your calendar that the table changed.

Templates

  • Any spreadsheet. Six rows, four channel columns, three numbers per cell. Date it.

Pitfalls

  • Comparing categories by volume. Ten hand-researched emails that produce two meetings beat a hundred cold ones that produce none.
  • Adding tracking to “get more data”. It changes how the message is treated and what you promised in the footer.

04 — Compliance essentials

Compliance box

United States commercial email, phone and lists: the essentials

  • Truthful header and routing. From and the originating domain identify the real sender. Your outreach domain is yours and resolves to your real business.
  • Truthful subject line. It reflects the content. No “Re:” on a first touch, no fake replies, no invented urgency.
  • Identify the message as an advertisement or solicitation, clearly and conspicuously. One plain sentence in the footer does it.
  • Include your valid physical postal address in every message.
  • A working opt-out in plain words that a recipient can use by replying or writing to one address, kept working for at least 30 days after the send, honored within 10 business days, with no fee, no login and no questions. Treat the next sync as the target; ten business days is the legal maximum.
  • You are responsible even if someone else sends for you. A tool or a contractor does not move the obligation.
  • No cold text messages and no autodialed or prerecorded calls without prior express written consent. In the US, business texting from ordinary numbers also requires carrier registration. Keep those channels out of the sender entirely; the only code paths it has are email.
  • Third-party lists need written provenance: who collected each row, from where, under what notice. Without that, the list stays frozen.
  • Keep one shared suppression list across every sender, every mailbox and every person who sends on your behalf, with the reason and the date for each entry.

This is a working summary of how we built the system to behave, not legal advice. Check your own state and jurisdiction: state laws add requirements, other countries have stricter regimes, and regulated customers (medical, financial, licensed trades) often need a conversation with counsel before the first wave.

05 — Copy and paste

Copy-paste templates

Every placeholder is in UPPER_CASE, in {braces}, or uses an example domain or an obvious stand-in. Replace all of them; before you save, search for example, your-, Your , YOUR_, SELECTOR, PASTE_, 123 , NN and {.

DNS records for the outreach domain

Your mail provider supplies the SPF include, the DKIM selector and the public key. Values below are shapes, not settings.

# SPF: who may send for this domain (your provider's include goes here)
your-outreach-domain.com.                       TXT  "v=spf1 include:_spf.your-mail-provider.example ~all"

# DKIM: SELECTOR and the public key come from your provider's admin console
SELECTOR._domainkey.your-outreach-domain.com.   TXT  "v=DKIM1; k=rsa; p=PASTE_PUBLIC_KEY_HERE"

# DMARC: start at p=none, watch the reports (through a viewer) for two weeks, then p=quarantine
# rua= is where reports go: your dmarc@ alias, or the address your report viewer gives you
_dmarc.your-outreach-domain.com.                TXT  "v=DMARC1; p=none; rua=mailto:dmarc@your-outreach-domain.com; adkim=s; aspf=s"

# Not a DNS record: web forwarding for the bare domain, set in the registrar or host panel
#   your-outreach-domain.com  ->  https://your-real-website.example

nginx: two routes to the bridge, everything else refused

Two snippets, because nginx accepts the throttle zones only at the top level. The bridge listens on a local unix socket (a file on the machine, not a network port); nothing outside the machine can reach it except through the two locations.

Snippet 1 goes in an http-level file, for example /etc/nginx/conf.d/YOUR_FORM-limits.conf:

# Throttle zones, per client. NN is requests per minute: pick small numbers and tune them.
limit_req_zone $binary_remote_addr zone=YOUR_FORM_config:10m rate=NNr/m;
limit_req_zone $binary_remote_addr zone=YOUR_FORM_submit:10m rate=NNr/m;

Snippet 2 goes inside your existing TLS server { } block:

location = /api/YOUR_FORM/config {
    limit_req zone=YOUR_FORM_config burst=N nodelay;
    proxy_pass http://unix:/run/lead-bridge/bridge.sock;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

location = /api/YOUR_FORM/submit {
    limit_req zone=YOUR_FORM_submit burst=N nodelay;
    client_max_body_size SIZE;        # a form is a few kilobytes; anything larger is refused
    proxy_pass http://unix:/run/lead-bridge/bridge.sock;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

# Every other /api/ path is refused outright.
location /api/ { return 403; }

Then nginx -t, and reload only if it passes.

systemd unit skeleton for the bridge

A dedicated user, a hardened sandbox, and the CRM credential handed to the process by systemd rather than written into a config file.

[Unit]
Description=Lead form bridge (website form -> CRM)
After=network-online.target
Wants=network-online.target

[Service]
User=lead-bridge
Group=lead-bridge
WorkingDirectory=/opt/lead-bridge
ExecStart=/usr/bin/python3 /opt/lead-bridge/bridge.py --socket /run/lead-bridge/bridge.sock
RuntimeDirectory=lead-bridge
StateDirectory=lead-bridge
# The CRM credential is visible to this process only, in memory, at $CREDENTIALS_DIRECTORY/crm_token
LoadCredential=crm_token:/etc/credstore/lead-bridge-crm-token
Environment=CRM_BASE_URL=https://crm.your-company.example
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
Restart=on-failure

[Install]
WantedBy=multi-user.target

Before the first start, create the credential file as root: /etc/credstore/lead-bridge-crm-token, mode 0600, owned by root, containing only the token and nothing else. Then systemctl daemon-reload and systemctl enable --now lead-bridge.

The 15-column CSV

The contract between the verify step and the sender. Finders fill the first six; the finder and verifier agents fill the rest.

id,source,name,category,address,website,email,email_type,verified,source_url,contact_name,still_operating,status,note,verify_note
id, source
Your row id and the finder or list the row came from (hand research, registry, places, website form).
name, category, address, website
The business as the finder recorded it. The street address is what the verifier must find on the source page.
email, email_type
The published address and whether it is on the business's own domain, a role address, or a free-mail address.
verified, source_url
yes only if the verifier re-opened source_url and confirmed business, address and email on the page.
contact_name
Only if published alongside the email. Never inferred.
still_operating
yes, no or unknown, from the page. Only yes is eligible.
status, note, verify_note
Your pipeline status, the finder's note, and the verifier's one-line reason.

Plain-text email skeleton

One personal line that proves you looked, two sentences about you, one easy question, then the footer the structure check requires.

From: Your Name <yourname@your-outreach-domain.com>
To: contact@their-business.example
Subject: A question about {business_name}'s {specific_thing}
List-Unsubscribe: <mailto:unsubscribe+{recipient_token}@your-outreach-domain.com>
Content-Type: text/plain; charset=utf-8

Hi {first_name_or_team},

{One sentence that proves you looked: something specific from their website or listing.}

{Two sentences: who you are and the one thing you help businesses like theirs with.}

{One question that is easy to answer with a yes or a no.}

Your Name
Your Business
https://your-real-website.example

--
This is a commercial message from Your Business, 123 Your Street, Your Town, ST 00000.
If you would rather not hear from us, reply with "unsubscribe" or write to
unsubscribe@your-outreach-domain.com and we will stop.

The approval task body

What plan writes into the task tracker. The person approving sees exactly what will go out, and the sender reads the result back before every message.

Title:  Approve email wave {wave_id}: {category}, {N} recipients

Category:      hand-researched (example: landscaping, property managers)
Recipients:    {N}; every row has verified=yes, an http(s) source_url, still_operating=yes
Window:        weekdays, {start}-{end} {time zone}; warm-up cap today: {cap}
Subject:       {subject}
Body:          {the full rendered body for one recipient, slots filled}
Content hash:  {sha256 over recipients + subject + body}
Confirmation:  APPROVE-{first 12 characters of the hash}

To approve: mark this task done and put the confirmation word in the result
(the task's free-text completion field). To cancel: set the task to blocked.
Anything else, including "ok" in a chat, is not an approval.
Editing the list or the copy changes the hash and cancels this approval; plan a new wave.
This approval expires after {max_age_days} days.

Category brief outline

One page per funnel category. Travels with the rows into the sender.

Category:               {hand-raisers | hand-researched | public registry | scraped | third-party (frozen) | partners}
How they came to exist: {which finder, which filter, when}
Consent posture:        {they asked | cold but specific | cold, no email on file | frozen | direct B2B}
Best channel, and why:  {channel}: {one sentence of reasoning}
Second channel:         {channel, and the condition for using it}
Do-nots:                {three to five lines}
Copy brief:             {tone; length; the one ask}
Personalization slots:  {slot_1}: {where its content comes from}
                        {slot_2}: {where its content comes from}
Find more like this:    {finder to run, filter to apply}

06 — Learned the hard way

Pitfalls we hit, generalized

  • Agents report “done” without evidence. Make the verifier quote the source URL and the text it matched, and spot-check by hand before every import.
  • Free-text outputs drift. A JSON schema on every agent's output is cheap and catches most of it.
  • Parallel agents that share a parent context drift together. Give the verifier a fresh context for every chunk.
  • The address hiding in page code. Our verifier's first real catch was a batch of addresses that lived only in pages' structured data, a network-wide contact for a group of locations, which a finder reading page source had accepted because the business and the street address matched. “Shown as a contact on the page” is now in the prompt.
  • Registry pages change silently. Count rows per run and fail the run when the count drops sharply.
  • A spend cap in a vendor console is not a cap. The cap must be in the code path that makes the call.
  • Send-only scope sounds safer. It is the wrong choice: you cannot honor opt-outs you cannot read.
  • Retrying an unknown outcome. Write a reserved row before the call; if the outcome is unknown, leave it unsettled for a person.
  • Approving a list, not a message. Hash recipients plus subject plus body, and expire approvals, or an edit after approval quietly changes what goes out.
  • The Promotions tab is not a failure. It is where cold mail from a new domain lands at first.
  • The token only in the body. Put the per-recipient token in the List-Unsubscribe header and in the local part of the address, so an opt-out can be matched even when the body is gone.
  • A second sender without the shared suppression list will eventually mail someone who opted out of the first.
  • Audit rows and suppressions in different stores will disagree one day. Keep them together.
  • Ten business days is a maximum, not a target. The next sync is the target.
  • A secret pasted into a prompt or a chat. Transcripts persist. Rotate it the same day, and never do it again.

07 — On one page

Rebuild checklist

Times assume one person working part-time with a coding agent. Costs are approximate 2026 list prices for the fixed services and should be checked; your AI subscription and your own time are the dominant spend and are not included.

The nine pieces to build, what each needs, rough time and rough cost
#PieceWhat to set upRough timeRough cost
1Domain, mailbox, DNSDedicated outreach domain; one mailbox; SPF, DKIM aligned to the domain's own key, DMARC; unsubscribe and dmarc aliases; plus-addressing confirmed on the alias; send-plus-read API access.2 hours of work, then about two weeks of waiting (DMARC reports, warm-up) that overlaps Phases 2–6Domain about $10–20 a year; mailbox about $7–14 a month
2CRM and task trackerLead record with source and source_url; the five API calls; a one-line CLI; narrowest-scope credential in a secrets manager.Hours to adopt; days to self-hostSubscription, or hosting if self-hosted (see 3)
3Website form and bridgeOne form with consent and honeypot; server-side bridge with origin, CSRF, size, field and honeypot checks; receipt ledger; two allowed routes; throttles; systemd unit with the credential mounted in.About a dayA small server, about $6–24 a month, shared with the CRM
4FindersRegistry and license-roll collection with row counts; maps data behind a spend cap in code; hand lists in the same CSV shape; partner list kept separate.Registry a day or two; hand lists an afternoonCapped API spend only
5Verify workflowFinder and verifier prompts, a JSON schema, chunking of about a dozen rows, survivors and rejects files, a yield log.Half a day to set up, then per runAI subscription
6Funnel briefs and checksSix category briefs; claims checklist; structure check; both run on rendered text.Half a dayNone
7The senderCLI with a small database; provenance guard at import; preview; plan with hashed approval task; the check chain; reserved rows; warm-up, window, spacing; sync; circuit breaker; canary gate; fake transport and tests.1–2 weeks with an agent, plus a hostile review before first useNone beyond AI and time
8Canary and first waveWave of one to yourself; headers read; reply and opt-out paths exercised; canary date recorded; first wave of ten or fewer from one category.One week of calendar timeNone
9Friday scoreboardCategory by channel; delivered, genuine replies, meetings; one decision a week.Twenty minutes a weekNone

Totals: roughly three to five weeks of one person's part-time effort with an agent, and about $15–40 a month in fixed services. The expensive parts are not the services. They are the tests, the hostile review of the sender, and the discipline of approving every wave in the tracker instead of in a chat.

08 — In fairness

Honest limits

  • A fresh install sends nothing until a person acts. The canary gate stays closed until an operator records the date the canary was checked. That is deliberate, and it means the system cannot be handed to someone and left to run.
  • No one-click unsubscribe over HTTPS yet. The mailto List-Unsubscribe header and the visible opt-out line are in place; the HTTPS one-click variant that large mailbox providers expect from bulk senders is not. At tens of messages a day you are below those thresholds, but add it when you can.
  • DKIM alignment may need a console step. A DKIM pass on your provider's fallback key is not alignment with your domain. Our own canary passed that way first; the alignment step is a separate item in the provider's admin console.
  • The sender does not find. Every address it can mail came through Phase 5 with a source URL, or it is stored as not eligible and never planned.
  • The CRM dedupes on email. Two people sharing an address become one lead.
  • No alert channel for new inbound leads in the version described here. A person has to look at the CRM.
  • One sender, one mailbox. The check chain is enforced in that sender. If you add another, share the suppression list and the stop file, or do not add another.
  • We cannot tell you how many replies to expect. This guide describes how the system is built and governed. Our own first waves are the measurement, and we will say what they showed once there is something to say.
  • Not legal advice. The compliance box is a working summary. Regulated customers and other jurisdictions need counsel.