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:
parent
4d3af2aeae
commit
134a2611f6
2 changed files with 196 additions and 0 deletions
57
data/corporate_campaigns/otc_reincorporation.html
Normal file
57
data/corporate_campaigns/otc_reincorporation.html
Normal 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> — 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;">
|
||||||
|
• Plan of conversion / domestication + Texas certificate of formation<br>
|
||||||
|
• Board + shareholder approvals and the SEC disclosure around it<br>
|
||||||
|
• A Texas <strong>registered agent</strong> and (if you keep operations elsewhere) foreign qualification<br>
|
||||||
|
• 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 — fixed fees, no billable hours — 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 & pricing →</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 — 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. · 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 · <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>
|
||||||
139
scripts/build_otc_campaign.py
Normal file
139
scripts/build_otc_campaign.py
Normal 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())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue