# Carrier Leads

Written 2026-09-25. Nash: "Those are leads. They could be dispatchers or drivers… convert
them into carrier leads and create the same strategy that we had for broker leads: a
separate page where we manage those leads, emails, campaigns, and try to get them to join."

## What a carrier lead is

A person who used Synchron Permits as a dispatcher or driver but has **no carrier
relationship** in the export (580 people on 2026-09-25). They are not imported as unclaimed
profiles (nothing to attach); they are leads to convert into HeavyHaul Agent users.

## One engine, two kinds

The Freight Broker Leads manager was not copied. Every table, rule and screen it uses now
carries a `lead_kind` (`broker` | `carrier`, migration 0039) and the Carrier Leads page is
the same manager pointed at kind `carrier`.

| | Freight Broker Leads | Carrier Leads |
|---|---|---|
| Page | `/admin/broker-leads` | `/admin/carrier-leads` |
| API | `/api/admin/broker-leads/*` | `/api/admin/carrier-leads/*` |
| Moderator surface | `broker_leads` | `carrier_leads` |
| Campaign type | `broker_recruitment` | `carrier_recruitment` |
| Template category | Freight broker invitations | Carrier invitations (`carrier_opportunities`) |
| Shipped templates | `broker_lead_invitation` / `_reminder` / `_final_notice` | `carrier_lead_invitation` / `_reminder` / `_final_notice` |
| Settings row | `broker_leads_settings` id 1 | id 2 |
| Conversion | relation confirmed | account created |
| Account from the join link | freight broker → `/fb-dashboard` | dispatcher → `/cd-dashboard`, driver → `/ct-dashboard` |

Shared and unchanged: `broker_agent_leads` (the table name predates the second kind),
`broker_lead_events`, `broker_lead_segments`, `email_campaigns`, `email_campaign_recipients`,
the ZeptoMail webhook, unsubscribe and suppression, the frequency cap, send windows, warm-up
ramps, the hourly scheduler, analytics and CSV export.

Carrier-specific columns on the lead: `lead_type` (`dispatcher` | `driver` | `both`),
`company_name` (free text from the source), `source_record_id` (Synchron user id),
`source_status_active`, `source_created_at`. Carrier-specific filters: type, still active in
Synchron. Broker-only filters (brokerage, domain check, state) are hidden on the carrier page.
Template variables for carrier templates: `first_name`, `last_name`, `full_name`,
`company_name`, `lead_type`, `join_link`, `unsubscribe_link`, `preferences_link`,
`support_email`, `company_footer`.

## Loading the Synchron people

```
node scripts/verify-migration-0039.mjs
node scripts/synchron-people/carrier-leads.mjs imports/synchron-people-2026-09-25            # dry run
node scripts/synchron-people/carrier-leads.mjs imports/synchron-people-2026-09-25 --apply
```

Reads `plan.json` (the people the import deferred) and the CSVs; writes one lead per person,
idempotent on the Synchron user id. Skips anyone without an email, anyone who already has an
HHA account, and any email already held by a broker lead. Role mailboxes are marked
`mailbox_type = role`. Tags: the lead type and `active` / `inactive`. No account, no email.

## What the campaign says

Nash, 2026-09-25: "They did create an account on Synchron, but they never used us most
likely. And we should not advertise as a Synchron." The carrier campaign therefore claims no
previous relationship and never names the permit service. It sells the free account:

- **upload any permit** — from a dispatcher, a broker, a permit service or the state;
- **free alerts** when the permit and the load disagree, or the permit is expiring or expired;
- **provisions in plain language** — travel windows, metro curfews, weekend and holiday bans, flags, signs, lights, escorts;
- **ask the agent about that permit** and get the answer with the line it came from;
- **order a Google Maps route** from a permit you uploaded — parts, a GPX file for the truck GPS, a Hummer GPS link, a pin for every exit.

Uploading, the alerts, the provisions and the questions are free; only the route is paid, and
the emails say so. The join page carries the same four points. A test in
`tests/broker-leads.test.ts` fails if a carrier template mentions the permit service, claims
past business, drops the join link or uses a variable that can render empty.

## Screening the junk before anything sends

The source system let anyone register, so the deferred people include a bot wave: 269 of the
580 have random-consonant names, not one has a company, 266 registered as dispatcher *and*
driver, and they all landed in one burst in 2025. Emailing those costs deliverability.

```
node scripts/synchron-people/screen-carrier-leads.mjs imports/synchron-people-2026-09-25            # dry run
node scripts/synchron-people/screen-carrier-leads.mjs imports/synchron-people-2026-09-25 --apply
```

Each match becomes `claim_status = 'manual_review'` with the reason: the lead stays visible on
the Leads tab under "to review", every campaign skips it, and a person can Accept (back in the
pool) or Drop (suppressed). Nothing is deleted, and the pass is idempotent. Rules: auto-generated
name, test wording, a bot by its own name, our own staff address or mailbox family, a domain that
cannot receive mail, a placeholder address, first and last name the same word, no name at all.

On 2026-09-25 this held 299 and left 281 mailable.

## The first campaign

```
node scripts/seed-carrier-lead-campaign.mjs            # dry run: audience, steps, throttle
node scripts/seed-carrier-lead-campaign.mjs --apply    # creates the DRAFT
```

"Carrier wave 1 — upload a permit": invitation, reminder after 6 days, last note after 14, to
carrier leads never emailed and still active in the source (229 after screening). Business hours in the lead's own
time zone, warming up from 50 a day to 300, one email per lead every 7 days. It is a draft and
nothing is enrolled: open it, enrol the audience, send yourself a test, then approve. Automatic
sending also needs the switch at the top of the page.

## Scheduler

The hourly cron must call both kinds:

```
POST /api/admin/broker-leads/run    x-campaign-cron-secret: <CAMPAIGN_CRON_SECRET>
POST /api/admin/carrier-leads/run   x-campaign-cron-secret: <CAMPAIGN_CRON_SECRET>
```

Each kind has its own on/off switch on its page.
