Most active underwriters in 424B4 prospectuses
Count which banks appear on the cover of 2026 final prospectuses, which are listed first, and which banks work together most often.
Every 424B4 final prospectus names its underwriters on the cover page. /v1/ipo returns them in terms.underwriters, in the order they appear. This tutorial counts the banks across all 2026 deals, notes which bank is listed first, and finds the pairs that share covers most often.
Cleaning the names
The underwriter list is read from cover page text, so it carries some layout words along with the bank names: headings like "Global Coordinators" or "Joint Book-Runners", notes like "(in alphabetical order)", and footnote asterisks. The same bank also appears under several legal names ("Goldman Sachs & Co. LLC", "Goldman Sachs International"). The script drops the layout words and maps common names to one label.
The code
import os
import re
from collections import Counter
import requests
BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}
filings, start = [], 0
while True:
r = requests.get(f"{BASE}/v1/ipo", headers=HEADERS, timeout=300, params={
"formType": "424B4", "startDate": "2026-01-01", "endDate": "2026-10-12", "size": 25, "from": start})
r.raise_for_status()
page = r.json()["filings"]
if not page:
break
filings += page
start += 25
NOISE = re.compile(r"order|coordinator|book.?runner|joint|manager|co-|underwriter|^\(|^and$", re.I)
ALIASES = {"goldman sachs": "Goldman Sachs", "j.p. morgan": "J.P. Morgan", "morgan stanley": "Morgan Stanley", "bofa": "BofA Securities",
"citigroup": "Citigroup", "jefferies": "Jefferies", "barclays": "Barclays", "evercore": "Evercore ISI", "ubs": "UBS",
"rbc": "RBC Capital Markets", "wells fargo": "Wells Fargo Securities", "leerink": "Leerink Partners", "td cowen": "TD Cowen"}
def clean(name):
name = name.strip().rstrip("*").strip()
if not name or NOISE.search(name):
return None
low = name.lower()
return next((v for k, v in ALIASES.items() if k in low), re.sub(r",?\s+(LLC|Inc\.?|L\.P\.|Ltd\.?)$", "", name))
deals, seen = [], set()
for f in filings:
t = f.get("terms") or {}
key = (f["cik"], t.get("sharesOffered"), t.get("pricePerShare"))
if key in seen:
continue
seen.add(key)
banks = []
for u in t.get("underwriters") or []:
c = clean(u)
if c and c not in banks:
banks.append(c)
if banks:
deals.append({**f, "banks": banks})
print(f"{len(seen)} distinct 424B4 deals in 2026, {len(deals)} with underwriters on the cover page")
lead = Counter(d["banks"][0] for d in deals)
anyrole = Counter(b for d in deals for b in d["banks"])
print(f"\n{'Underwriter':28}{'Deals':>7}{'Listed first':>14}")
for bank, n in anyrole.most_common(15):
print(f"{bank:28}{n:>7}{lead.get(bank, 0):>14}")
print("\nMost common pairs on the same cover:")
pairs = Counter(tuple(sorted(p)) for d in deals for i, a in enumerate(d["banks"]) for p in [(a, b) for b in d["banks"][i + 1:]])
for (a, b), n in pairs.most_common(5):
print(f" {n:3} {a} + {b}")
Output
Run on 2026-10-13 (about two minutes, since every cover page is read).
431 distinct 424B4 deals in 2026, 139 with underwriters on the cover page
Underwriter Deals Listed first
J.P. Morgan 44 15
Goldman Sachs 43 32
BofA Securities 39 7
Jefferies 36 8
Morgan Stanley 35 10
Barclays 27 1
Citigroup 25 2
RBC Capital Markets 25 0
Stifel 21 1
Evercore ISI 19 0
Wolfe | Nomura Alliance 18 0
Baird 17 0
William Blair 17 0
Wells Fargo Securities 17 1
UBS 17 1
Most common pairs on the same cover:
29 BofA Securities + Goldman Sachs
28 Goldman Sachs + J.P. Morgan
27 BofA Securities + J.P. Morgan
24 Barclays + Goldman Sachs
21 J.P. Morgan + Morgan Stanley
Reading the result
- Only 139 of 431 deals have underwriters extracted. Many small offerings use a single placement agent named in a format the parser does not pick up, and some covers are images. The ranking describes the larger, syndicated deals.
- "Listed first" is the first bank on the cover after cleaning. Many covers list bookrunners in alphabetical order (the note is often printed on the cover), so first position is a weak signal of the lead left bank. Treat it as an approximation.
- Counts are deals, not dollars. Weight by deal size with the gross proceeds from the largest offerings tutorial to rank by volume.
Variations
- Dollar weighted league table: join each deal's
sharesOffered * pricePerShareto its bank list and sum per bank. - Sector view: look up each issuer's SIC code with
/v1/entities?cik=<cik>and count banks per industry. - Track a bank over time: filter
dealsto those that include one bank and group by month offiledAt.