otc: reincorporation email template + campaign builder

otc_reincorporation.html: redomesticate-to-Texas hook (Business Court + TXSE +
DE franchise-tax cost) personalized by state_inc_name/company/ticker, cross-sell
RA/foreign-qual/annual-report/franchise-tax, same-day coupon, lead-capture CTA to
/contact?service=reincorporation (high-touch corporate service, not self-serve),
careful 'not a law firm / not legal advice' disclaimers + CAN-SPAM address.

build_otc_campaign.py: emails only verified-email issuers from the harvest+scrape
+verify pipeline, --de-nv-only for the best reincorp fit, reuses trucking sender
plumbing + coupon. Per-deal value is high so capped modestly (400/run default).
This commit is contained in:
justin 2026-06-14 06:58:43 -05:00
parent 4d3af2aeae
commit 134a2611f6
2 changed files with 196 additions and 0 deletions

View file

@ -0,0 +1,57 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>@media only screen and (max-width:600px){.pw-wrap{width:100%!important;border-radius:0!important;}.pw-pad{padding:24px 16px!important;}}body,table,td,p,a{-webkit-text-size-adjust:100%;}table{border-collapse:collapse!important;}img{border:0;}</style></head><body style="margin:0;padding:0;background:#eef0f3;">
<center>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#eef0f3;"><tr><td style="padding:24px 10px;">
<table role="presentation" class="pw-wrap" width="620" cellpadding="0" cellspacing="0" style="margin:0 auto;border-radius:10px;overflow:hidden;background:#fff;">
<!-- Header -->
<tr><td style="background:#1a2744;padding:26px 28px;">
<img src="https://performancewest.net/images/logo.png" alt="Performance West" style="height:42px;margin-bottom:10px;display:block" />
<h1 style="color:#fff;margin:0;font-size:21px;font-weight:700;font-family:Inter,system-ui,sans-serif;">Still incorporated in {{ .Subscriber.Attribs.state_inc_name }}?</h1>
<p style="color:#94a3b8;margin:6px 0 0;font-size:13px;font-family:Inter,system-ui,sans-serif;">A note for {{ .Subscriber.Attribs.company }} ({{ .Subscriber.Attribs.ticker }})</p>
</td></tr>
<!-- Body -->
<tr><td class="pw-pad" style="padding:28px;font-family:Inter,system-ui,sans-serif;color:#1f2937;">
<p style="font-size:15px;margin:0 0 16px;line-height:1.6;">To the corporate / finance team at <strong>{{ .Subscriber.Attribs.company }}</strong>,</p>
<p style="font-size:14px;line-height:1.7;margin:0 0 16px;">A growing number of public companies are <strong>redomesticating out of {{ .Subscriber.Attribs.state_inc_name }} to Texas</strong> &mdash; driven by the new Texas Business Court (specialized corporate bench, live 2024), the Texas Stock Exchange, and Texas's lower ongoing cost (no annual franchise tax for most entities under the revenue threshold, versus Delaware's franchise tax that can run into five figures for public companies).</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:20px 0;"><tr><td style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:10px;padding:18px;">
<h3 style="margin:0 0 10px;font-size:15px;color:#075985;font-weight:700;">What a reincorporation actually involves</h3>
<div style="font-size:13px;color:#0c4a6e;line-height:1.8;">
&bull; Plan of conversion / domestication + Texas certificate of formation<br>
&bull; Board + shareholder approvals and the SEC disclosure around it<br>
&bull; A Texas <strong>registered agent</strong> and (if you keep operations elsewhere) foreign qualification<br>
&bull; Winding down the old-state franchise-tax / annual-report obligations
</div>
</td></tr></table>
<p style="font-size:14px;line-height:1.7;margin:0 0 18px;">We handle the filing mechanics end to end &mdash; fixed fees, no billable hours &mdash; and coordinate with your counsel on the corporate approvals. If a move is not on the table this year, we also handle the everyday items that keep your entity in good standing: <strong>registered agent, annual reports, franchise tax, and foreign qualification</strong> in any state you operate.</p>
{{ if .Subscriber.Attribs.coupon_code }}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:20px 0;"><tr><td style="background:#fff7ed;border:2px solid #f97316;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:13px;font-weight:700;color:#9a3412;letter-spacing:.04em;margin:0 0 6px">{{ .Subscriber.Attribs.coupon_pct }}% OFF OUR SERVICE FEE - THIS WEEK</p>
<p style="font-size:14px;color:#9a3412;margin:0 0 4px">Use code <strong style="font-size:16px;letter-spacing:.08em">{{ .Subscriber.Attribs.coupon_code }}</strong> on any filing.</p>
<p style="font-size:12px;color:#b91c1c;font-weight:700;margin:0">Expires {{ .Subscriber.Attribs.coupon_expires }}. Applies to our service fee; state filing fees are separate.</p>
</td></tr></table>
{{ end }}
<!-- CTA -->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#eff6ff;border:2px solid #2563eb;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#1e3a8a;margin:0 0 12px;font-weight:600;">Want a straight answer on whether a Texas reincorporation pencils out for {{ .Subscriber.Attribs.ticker }}?</p>
<a href="{{ .Subscriber.Attribs.lp_link }}" style="display:inline-block;padding:14px 36px;background:#2563eb;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">See options &amp; pricing &rarr;</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f8fafc;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">
<strong>Prefer to talk it through?</strong> Reply to this email or call <strong>(888) 411-0383</strong>. We are a corporate compliance firm; we are not your attorney and this is not legal advice &mdash; we handle the filings and coordinate with your counsel.
</td></tr></table>
</td></tr>
<!-- Footer -->
<tr><td style="padding:16px 28px;background:#f8fafc;border-top:1px solid #e5e7eb;font-size:11px;color:#9ca3af;text-align:center;">
<p style="margin:0 0 8px;">Performance West Inc. is a corporate filing/compliance firm, not a law firm. This message is general information, not legal advice.</p>
<p style="margin:0;">Performance West Inc. &middot; 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 &middot; <a href="https://performancewest.net" style="color:#6b7280;">performancewest.net</a></p>
<p style="margin:6px 0 0;"><a href="{{ UnsubscribeURL }}" style="color:#6b7280;">Unsubscribe</a></p>
</td></tr>
</table></td></tr></table></center></body></html>

View file

@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Build the OTC issuer reincorporation/compliance outreach campaign.
Audience: US-domestic OTC SEC issuers (from harvest_otc_issuers.py +
scrape_otc_emails.py + verify). Hook: redomesticate-to-Texas for DE/NV issuers;
cross-sell registered agent / foreign qualification / annual report / franchise
tax. High per-deal value, small universe (~700-900), so NOT warmup-capped the
same way -- but still MX-aware and same-day-coupon enabled. Reuses the trucking
sender plumbing.
Input CSV (verified): cik,name,ticker,state_inc,phone,city,state,zip,domain,email
plus verify columns. Only rows with a verified email are emailed.
Usage:
python3 scripts/build_otc_campaign.py VERIFIED.csv --start-campaign
python3 scripts/build_otc_campaign.py VERIFIED.csv --preview
python3 scripts/build_otc_campaign.py VERIFIED.csv --dry-run
"""
from __future__ import annotations
import argparse
import csv
import os
import sys
from datetime import datetime, timezone, timedelta, date
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
import psycopg2 # noqa: E402
import scripts.build_trucking_campaigns as tc # noqa: E402
LOG = tc.LOG
SOURCE_ENV = "CAMPAIGN_OTC_REINCORP_ID"
LP_LINK = f"{tc.SITE_DOMAIN}/contact?service=reincorporation"
STATE_NAMES = {
"DE": "Delaware", "NV": "Nevada", "CA": "California", "NY": "New York",
"FL": "Florida", "TX": "Texas", "CO": "Colorado", "WY": "Wyoming",
"MD": "Maryland", "IA": "Iowa", "MN": "Minnesota", "NJ": "New Jersey",
}
def verified_ok(r: dict) -> bool:
e = (r.get("email") or "").strip()
if not e or "@" not in e:
return False
vr = (r.get("verify_ok") or r.get("verify_reason") or "").strip().lower()
# Accept if verifier passed, or if the column is absent (treat present email
# as usable -- but prefer running verify first).
if "verify_ok" in r:
return str(r.get("verify_ok")).strip().upper() in ("Y", "YES", "TRUE", "1")
return True
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("infile")
ap.add_argument("--start-campaign", action="store_true")
ap.add_argument("--preview", action="store_true")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--limit", type=int, default=int(os.getenv("OTC_DAILY_CAP", "400")))
ap.add_argument("--de-nv-only", action="store_true",
help="only Delaware/Nevada issuers (best reincorp fit)")
args = ap.parse_args()
import logging
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s %(message)s")
rows = [r for r in csv.DictReader(open(args.infile, newline="", encoding="utf-8"))
if verified_ok(r)]
if args.de_nv_only:
rows = [r for r in rows if (r.get("state_inc") or "").strip() in ("DE", "NV")]
LOG.info("[otc] %d emailable issuers (de_nv_only=%s)", len(rows), args.de_nv_only)
src = os.getenv(SOURCE_ENV)
if not src:
LOG.error("[otc] %s not set -- create the OTC source campaign first", SOURCE_ENV)
return 2
base = tc.get_base_campaign(int(src))
conn = psycopg2.connect(tc.DB_URL)
coupon = None
if args.start_campaign and not args.preview and not args.dry_run:
try:
coupon = tc.get_or_create_daily_coupon(conn, date.today())
except Exception as exc: # noqa: BLE001
LOG.warning("[otc] coupon mint failed: %s", exc)
def attribs(r):
si = (r.get("state_inc") or "").strip()
return {
"company": r.get("name", ""), "ticker": r.get("ticker", ""),
"state_inc": si, "state_inc_name": STATE_NAMES.get(si, si or "your state"),
"lp_link": LP_LINK,
**tc.coupon_attribs(coupon),
}
if args.preview:
subs = [{"email": tc.TEST_EMAIL, "name": "Sample Issuer",
"attribs": attribs({"name": "Sample Issuer Inc.", "ticker": "SMPL",
"state_inc": "DE"})}]
else:
subs = []
seen = set()
for r in rows[:args.limit]:
e = r["email"].strip().lower()
if e in seen:
continue
seen.add(e)
subs.append({"email": e, "name": r.get("name", e), "attribs": attribs(r)})
today = date.today().isoformat()
list_name = f"OTC Reincorporation - {today}"
campaign_name = f"OTC Reincorporation - {date.today().strftime('%b %d %Y')}"
if args.dry_run:
LOG.info("[otc] DRY RUN -- would email %d issuers (coupon=%s)", len(subs), coupon)
return 0
list_id = tc.create_list(list_name)
added = tc.import_subscribers(list_id, subs)
LOG.info("[otc] list %d: %d/%d added", list_id, added, len(subs))
if added == 0:
LOG.error("[otc] 0 added; aborting")
return 1
send_at = datetime.now(timezone.utc) + timedelta(minutes=5)
cid = tc.create_and_schedule_campaign(
base, list_id, campaign_name, send_at, schedule=args.start_campaign and not args.preview)
LOG.info("[otc] campaign %d created (%s)", cid,
"scheduled" if args.start_campaign and not args.preview else "draft")
conn.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())