diff --git a/scripts/workers/campaign_helpers.py b/scripts/workers/campaign_helpers.py
new file mode 100644
index 0000000..026d28b
--- /dev/null
+++ b/scripts/workers/campaign_helpers.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python3
+"""
+campaign_helpers.py — Shared Listmonk campaign building blocks.
+
+Branded, email-client-safe HTML helpers (header, flag bar, stat tiles, CTA
+button, footer, paragraphs, lists) plus the Listmonk POST helper. Extracted
+from create_campaigns.py so multiple campaign scripts (the original 7-email
+drips and one-off promos like the CRTC USF campaign) can share ONE source of
+truth for the look-and-feel and the API plumbing.
+
+Importing this module has NO side effects (it does not POST anything).
+"""
+
+import sys
+
+import requests
+
+# ── Listmonk connection ──────────────────────────────────────────────────────
+LISTMONK_URL = "https://lists.performancewest.net"
+AUTH = ("api", "6X1rKPea61N4rZ1S65Hx5zvqzbCj30F6nvEe9oVGH_Y")
+
+# ── Brand constants ──────────────────────────────────────────────────────────
+URL = "https://performancewest.net/order/canada-crtc"
+PHONE = "1-888-411-0383"
+EMAIL = "info@performancewest.net"
+CONTACT = (
+ f'Email {EMAIL} '
+ f'or call {PHONE}'
+)
+
+
+# ── HTML helpers ─────────────────────────────────────────────────────────────
+
+def hdr(eyebrow, headline, sub=None):
+ s = f'
{sub}
' if sub else ''
+ return (
+ ''
+ ''
+ ''
+ ' | '
+ 'Telecom Services | '
+ '
| '
+ ''
+ '| '
+ f' {eyebrow} '
+ f'{headline}{s}'
+ ' |
|
'
+ )
+
+
+def flagbar(left, right):
+ return (
+ ''
+ ''
+ ' '
+ f'{left} | '
+ '→ | '
+ ' '
+ f'{right} | '
+ '
|
'
+ )
+
+
+def stats(*items):
+ w = 100 // len(items)
+ cells = "".join(
+ f''
+ f'| '
+ f' {v} '
+ f'{l} '
+ ' |
| '
+ for v, l in items
+ )
+ return f''
+
+
+def cta(text, url):
+ return (
+ ''
+ )
+
+
+def carriers_block():
+ data = [
+ ("Twilio","NASDAQ: TWLO"),("Bandwidth","NASDAQ: BAND"),("Telnyx","Washington DC"),("RingCentral","NYSE: RNG"),
+ ("Vonage","Holmdel NJ"),("8x8","NASDAQ: EGHT"),("Zoom Phone","NASDAQ: ZM"),("Dialpad","San Ramon CA"),
+ ("Google Voice","Alphabet / GOOGL"),("Ooma","NYSE: OOMA"),("Onvoy / Sinch","OMX: SINCH"),("Sangoma","NASDAQ: SANG"),
+ ]
+ rows = ""
+ for i in range(0, len(data), 4):
+ cells = "".join(
+ f''
+ f'| '
+ f' {n} '
+ f'{t} '
+ ' |
| '
+ for n, t in data[i:i+4]
+ )
+ rows += f"{cells}
"
+ return (
+ ''
+ '| '
+ ' '
+ 'Join other US voice carriers registered to do business in Canada '
+ ' '
+ f''
+ ' |
'
+ )
+
+
+def ftr(note=""):
+ note_html = f'{note}
' if note else ''
+ return (
+ ''
+ )
+
+
+def bq(t):
+ return (
+ ''
+ )
+
+
+def P(t):
+ return f'{t}
'
+
+
+def PS(t):
+ return f'{t}
'
+
+
+def H2(t):
+ return f'{t}
'
+
+
+def UL(*items):
+ return (
+ ''
+ + "".join(f'- {i}
' for i in items)
+ + '
'
+ )
+
+
+def assemble(hdr_html, fb_html, body_html, ftr_html):
+ inner = (
+ hdr_html + fb_html
+ + f''
+ + ftr_html
+ )
+ return (
+ ''
+ ''
+ )
+
+
+def create_campaign(name, subject, lists, body_html, altbody=None, status="draft"):
+ """POST a campaign to Listmonk.
+
+ altbody: optional plaintext alternative. Strongly recommended for
+ deliverability — an HTML-only campaign is a spam signal. Pass the output of
+ scripts._email_plaintext.html_to_text(body_html) (or a hand-written
+ plaintext). When omitted, Listmonk generates its own plaintext at send time.
+ """
+ s = requests.Session()
+ s.auth = AUTH
+ payload = {
+ "name": name, "subject": subject, "lists": lists,
+ "type": "regular", "content_type": "html",
+ "body": body_html, "status": status,
+ }
+ if altbody is not None:
+ payload["altbody"] = altbody
+ r = s.post(f"{LISTMONK_URL}/api/campaigns", json=payload, timeout=30)
+ if not r.ok:
+ print(f" ERROR {r.status_code}: {r.text[:200]}", file=sys.stderr)
+ return None
+ cid = r.json().get('data', {}).get('id', '?')
+ print(f" [{cid}] {name}")
+ return cid
diff --git a/scripts/workers/create_crtc_usf_campaign.py b/scripts/workers/create_crtc_usf_campaign.py
new file mode 100644
index 0000000..5eb99b5
--- /dev/null
+++ b/scripts/workers/create_crtc_usf_campaign.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+"""
+create_crtc_usf_campaign.py — One-off CRTC marketing email hooked on the
+Q3 2026 USF contribution-factor increase.
+
+Audience : Listmonk list 3 (FCC Carriers - Direct Contacts, ~6,046 enabled).
+Hook : Q3 2026 federal USF contribution factor = 38.8% (up from 37.0% in
+ Q2), effective Jul 1 (FCC PN DA-26-546A1).
+Offer : $200 off the CRTC carrier package service fee with code CANADA200,
+ valid through Fri Jun 19 2026 23:59 ET. The CRTC order page
+ auto-applies ?code= from the URL, so the CTA links carry it.
+Lead : "Canadian Wholesale Carrier & Vendor Reference Guide" PDF, hosted at
+magnet performancewest.net/guides/canada-carrier-guide.pdf (Listmonk
+ campaigns can't attach files, so we link a prominent download).
+
+Creates the campaign in Listmonk as a DRAFT. Sending is a separate, manual,
+STOP-and-confirm step in the Listmonk UI (or set status via API). Run:
+
+ python3 scripts/workers/create_crtc_usf_campaign.py # draft on list 3
+ python3 scripts/workers/create_crtc_usf_campaign.py --test # draft to a test list/email
+"""
+import argparse
+import os
+import sys
+
+# Make `scripts` importable whether run from repo root or scripts/workers.
+_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+if _REPO not in sys.path:
+ sys.path.insert(0, _REPO)
+
+from scripts._email_plaintext import html_to_text # noqa: E402
+from scripts.workers.campaign_helpers import ( # noqa: E402
+ CONTACT, P, PS, H2, UL, bq, cta, hdr, flagbar, stats, assemble, ftr,
+ create_campaign,
+)
+
+LIST_ID = 3
+CODE = "CANADA200"
+GUIDE_URL = "https://performancewest.net/guides/canada-carrier-guide.pdf"
+ORDER_URL = f"https://performancewest.net/order/canada-crtc?code={CODE}"
+
+
+def coupon_banner():
+ """Prominent $200-off banner with expiry."""
+ return (
+ ''
+ '| '
+ ' Limited-time offer '
+ f'$200 off your Canadian carrier setup '
+ f'Use code {CODE} at checkout · expires Friday at 11:59pm ET '
+ ' |
'
+ )
+
+
+def guide_block():
+ """PDF lead-magnet download block (Listmonk can't attach files)."""
+ return (
+ ''
+ ''
+ ''
+ '| 📄 | '
+ ''
+ ' Free guide: Canadian Wholesale Carrier & Vendor Reference '
+ '12 vetted Canadian wholesale partners for voice termination, DIDs, SIP trunking and UCaaS — the upstream vendors you’ll work with once your Canadian carrier is live. '
+ f'Download the PDF →'
+ ' | '
+ ' |
'
+ )
+
+
+def build_body():
+ return (
+ P("Hi {{ .Subscriber.FirstName }},")
+ + P("If you contribute to the federal Universal Service Fund, your Q3 number just went up again.")
+ + bq("The FCC has set the Q3 2026 USF contribution factor at 38.8% — up from 37.0% in Q2, and effective July 1. That is the rate you remit on the interstate and international end-user revenue you report on your 499.")
+ + P("38.8% is near the highest the factor has ever been. A decade ago it sat in the mid-teens. For a small or mid-size US carrier, that is a steadily rising tax on every interstate dollar you bill — on top of everything else the FCC requires.")
+ + stats(
+ ("38.8%", "Q3 2026 USF
contribution factor"),
+ ("+1.8 pts", "increase over
Q2 (37.0%)"),
+ ("Jul 1", "effective date
(FCC DA-26-546A1)"),
+ )
+ + H2("The US carrier burden, in one place.")
+ + P("USF is just the line item that moved this quarter. The full load a registered US carrier carries:")
+ + UL(
+ "USF contributions — now 38.8% of interstate/international end-user revenue, filed and remitted via the 499",
+ "FCC Form 499-A / 499-Q — annual and quarterly revenue filings, with true-ups and audit exposure",
+ "Robocall Mitigation Database — annual recertification; miss it and your traffic gets blocked",
+ "STIR/SHAKEN — call-authentication implementation and ongoing attestation",
+ "CALEA — lawful-intercept capability, SSI filing, and the cost of a compliant solution",
+ "Section 214 + Team Telecom — for international service, with national-security review that can stall financings and M&A",
+ "State PUC registrations and FCC regulatory fees on top of the federal load",
+ )
+ + H2("Why smaller carriers are standing up a Canadian operation.")
+ + P("A CRTC-registered Canadian carrier is a separate legal entity in a separate regulatory jurisdiction. For the voice traffic you move there, the US compliance stack simply does not apply:")
+ + UL(
+ "No USF. Canada funds its contribution program differently — there is no 38.8% factor on your Canadian carrier’s revenue",
+ "No Robocall Mitigation Database recert and no FCC 499 for the Canadian entity",
+ "No CALEA mandate in the US sense — lawful-intercept obligations are far lighter and cheaper",
+ "No Section 214 / Team Telecom — CRTC registration is a notification, not an application with a national-security review",
+ "Same +1 country code. Your customers dial exactly the same way — nothing changes on their end",
+ "A clean second jurisdiction — an FCC enforcement action against your US entity does not reach a Canadian corporation",
+ )
+ + bq("You do not give up your US business. You add a Canadian carrier alongside it — for the voice traffic that doesn’t need to sit under the FCC, and for the Canadian market you can now sell into.")
+ + H2("What we set up — turnkey, in 6–10 weeks.")
+ + UL(
+ "Incorporation in British Columbia or Ontario — a separate legal entity from your US company",
+ "CRTC registration (domestic reseller + BITS international authorization)",
+ "Canadian DID provisioned under your new carrier identity",
+ "Virtual registered office, .ca domain + up to 14 email addresses",
+ "Full corporate binder, delivered digitally — plus a Canadian business-banking referral",
+ )
+ + coupon_banner()
+ + cta(f"Start your Canadian carrier setup — $200 off →", ORDER_URL)
+ + guide_block()
+ + PS(f"Questions about how the Canadian structure would work for your traffic? {CONTACT}. The {CODE} discount is good through Friday at 11:59pm ET.")
+ + P("— Performance West")
+ )
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--test", action="store_true", help="create against a test list id (env CRTC_TEST_LIST) instead of list 3")
+ ap.add_argument("--name", default="CRTC USF Q3 \u2014 38.8% increase + $200 off (CANADA200)")
+ ap.add_argument("--subject", default="USF jumps to 38.8% in Q3 \u2014 here\u2019s the Canadian alternative ($200 off)")
+ args = ap.parse_args()
+
+ lists = [int(os.getenv("CRTC_TEST_LIST", "0"))] if args.test else [LIST_ID]
+ if args.test and lists == [0]:
+ print("--test requires CRTC_TEST_LIST env var (a test list id)", file=sys.stderr)
+ return 1
+
+ body = assemble(
+ hdr(
+ "USF Increase \u2014 Q3 2026",
+ "USF just hit 38.8%.
There’s a Canadian alternative.",
+ "The federal contribution factor rose again, effective July 1",
+ ),
+ flagbar(
+ "US carrier \u2014 38.8% USF + the full FCC stack",
+ "Canadian CRTC carrier \u2014 no USF, separate jurisdiction",
+ ),
+ build_body(),
+ ftr(""),
+ )
+ altbody = html_to_text(body)
+
+ print(f"=== Creating CRTC USF campaign (list {lists}) ===")
+ cid = create_campaign(args.name, args.subject, lists, body, altbody=altbody, status="draft")
+ if cid:
+ print(f"\nDraft created (id {cid}). Review/preview in Listmonk, then send manually.")
+ print(f"Body: {len(body):,} chars HTML / {len(altbody):,} chars plaintext")
+ return 0 if cid else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())