Build an M&A research agent

Superhighway guides

M&A intelligence — comparable transactions, deal multiples, the buyer universe, regulatory exposure — is some of the most expensive data in finance. PitchBook, CapIQ, and Refinitiv charge five and six figures a year for it. But much of the underlying signal is public: SEC 8-K deal announcements, press releases, league tables, investment-bank research, and news coverage. This guide builds a Python agent that mines those public sources and assembles a structured M&A brief. It chains all four Superhighway endpoints — /research for the deal landscape and buyer universe, /search for comparable transactions and SEC filings, /scrape for specific deal terms and rationale, and /news for recent announced deals and rumored targets — then uses an LLM to emit a structured M&A intelligence brief as JSON.

1. What you'll build

A Python agent that takes a company, sector, or deal type and produces a structured M&A intelligence brief:

2. Setup

pip install openai requests python-dotenv

Create a .env file with your two keys:

SUPERHIGHWAY_API_KEY=your_key_here
OPENAI_API_KEY=your_key_here

3. Research the M&A landscape

Start with /research, which pulls multi-source background into one synthesis — the deal history for this company or sector, how transactions are typically structured, who the active buyers are, and what valuation multiples apply. This is the grounding context for every later step.

import requests, os, json

SUPERHIGHWAY_KEY = os.getenv("SUPERHIGHWAY_API_KEY")
BASE = "https://superhighway.walls.sh"

def research_ma_landscape(company_or_sector: str) -> str:
    """Deep synthesis: deal history, structures, buyer universe, multiples."""
    r = requests.get(
        f"{BASE}/research",
        params={
            "q": f"{company_or_sector} M&A acquisition merger deal history valuation multiples",
            "pages": 6
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    data = r.json()
    return data.get("synthesis", data.get("markdown", ""))[:3000]

4. Find comparable transactions and filings

Two /search calls do the legwork. The first hunts for recent deal announcements and SEC 8-K filings; the second goes after valuation comps — EV/EBITDA, EV/Revenue, and the strategic buyers behind comparable deals. Together they surface the candidate pages the agent will scrape for hard terms.

def find_comparable_deals(company_or_sector: str) -> list[dict]:
    """Find recent deal announcements and SEC 8-K filings."""
    r = requests.get(
        f"{BASE}/search",
        params={
            "q": f"{company_or_sector} acquisition deal announcement SEC 8-K merger agreement 2024 2025",
            "limit": 6
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("results", [])

def find_valuation_comps(company_or_sector: str) -> list[dict]:
    """Find comp analysis: multiples, strategic acquirers, buyer universe."""
    r = requests.get(
        f"{BASE}/search",
        params={
            "q": f"{company_or_sector} strategic acquirer buyer M&A valuation EV/EBITDA revenue multiple comparable",
            "limit": 6
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("results", [])

5. Scrape deal pages and filings

/scrape turns each candidate URL into clean, LLM-ready markdown. This is where the agent pulls actual deal terms — purchase price, consideration mix, stated rationale — out of press releases, 8-K filings, and company IR pages.

def scrape_deal_page(url: str) -> dict:
    """Scrape a press release, filing, or IR page for deal terms and rationale."""
    r = requests.get(
        f"{BASE}/scrape",
        params={"url": url},
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    data = r.json()
    return {
        "url": url,
        "title": data.get("title", ""),
        "content": data.get("markdown", "")[:2500],
    }

6. Get recent M&A news

/news surfaces what just happened — newly announced deals, rumored takeover targets, strategic buyer activity, PE portfolio moves, and antitrust rulings. This is the time-sensitive layer that a static comp set misses.

def get_ma_news(company_or_sector: str) -> list[dict]:
    """Get recent announced deals, rumors, and strategic buyer activity."""
    r = requests.get(
        f"{BASE}/news",
        params={
            "q": f"{company_or_sector} merger acquisition deal rumor takeover strategic buyer",
            "count": 6
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("articles", [])

7. Generate the M&A brief with an LLM

Now hand everything to the LLM. The system prompt forbids inventing deal terms or multiples and bars any buy/sell or invest/divest recommendation — the agent summarizes what's in the sources, it does not advise. The output is structured JSON so it slots straight into a deal memo, a screening tracker, or a dashboard.

from openai import OpenAI

llm = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

DISCLAIMER = (
    "This summary is for informational purposes only and does not constitute "
    "financial or investment advice. M&A transactions involve significant "
    "complexity and risk. Always conduct proper due diligence and consult "
    "qualified financial advisors and legal counsel before making investment "
    "or acquisition decisions."
)

def generate_ma_brief(
    subject: str,
    landscape: str,
    deal_pages: list[dict],
    news: list[dict],
    context: dict
) -> dict | None:
    """Generate a structured M&A intelligence brief."""

    deal_text = "\n".join(
        f"- {p['title']}: {p['content'][:400]}"
        for p in deal_pages[:5]
        if p.get("content")
    )

    news_text = "\n".join(
        f"- {n.get('title', '')} ({n.get('source', '')})"
        for n in news[:6]
    )

    subject_type = context.get("subject_type", "company")
    deal_size = context.get("deal_size", "mid-market")
    geography = context.get("geography", "global")

    response = llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""You are an M&A analyst preparing an intelligence brief.
The subject is a {subject_type}; focus on {deal_size} deals in the {geography} market.
Be specific and factual. Only use information from the provided sources.
Do not invent deal terms or valuation multiples — if a figure isn't in the sources,
say 'not found in sources.' Do not make invest, divest, buy, or sell recommendations."""
            },
            {
                "role": "user",
                "content": f"""Write an M&A intelligence brief for: {subject}

M&A Landscape:
{landscape[:2000]}

Comparable Deals & Filings:
{deal_text}

Recent M&A News:
{news_text}

Return JSON with:
- subject: string (the company, sector, or deal type researched)
- deal_type: "strategic-acquisition" | "private-equity-buyout" | "merger-of-equals" | "carve-out" | "growth-investment" | "mixed"
- strategic_rationale: string (why buyers typically pursue this target/sector — scale, capabilities, market access, synergies)
- comparable_transactions: list of 3-5 strings (relevant precedent deals found, with rough terms if public)
- typical_multiples: string (what valuation metrics apply: EV/EBITDA, EV/Revenue, P/E — and typical ranges for this sector from the sources)
- potential_acquirers: list of 3-5 strings (who would logically buy — strategic buyers and PE firms)
- regulatory_considerations: string (antitrust risk, jurisdiction issues, approvals typically required)
- recent_deal_activity: list of 3 strings (from the news — recently announced deals, rumors, PE portfolio moves)
- due_diligence_flags: list of 3-4 strings (key things a buyer would examine: IP, customer concentration, tech debt, litigation, retention)
- data_quality: "high" | "medium" | "low" (how much source material was found)"""
            }
        ],
        response_format={"type": "json_object"}
    )

    try:
        brief = json.loads(response.choices[0].message.content)
        brief["disclaimer"] = DISCLAIMER
        return brief
    except (json.JSONDecodeError, KeyError):
        return None

8. The full research pipeline

Wire the steps together: research the landscape, find comparable deals and valuation comps, scrape the top pages, pull news, then generate the brief. The context dict tells the agent whether you're researching a company, a sector, or a deal type — and what deal size and geography to slant toward.

def research_ma(
    subject: str,
    context: dict | None = None,
    max_pages: int = 5
) -> dict | None:
    """
    Run the full M&A research pipeline.

    context: {
        "subject_type": "company" | "sector" | "deal-type",
        "deal_size": "small-cap" | "mid-market" | "large-cap",
        "geography": "US" | "EU" | "global"
    }
    """
    if context is None:
        context = {"subject_type": "company", "deal_size": "mid-market", "geography": "global"}

    print(f"Researching M&A: {subject}")

    # Step 1: Landscape synthesis
    print("Synthesizing M&A landscape...")
    landscape = research_ma_landscape(subject)

    # Step 2: Find comparable deals and valuation comps
    print("Finding comparable transactions and filings...")
    results = find_comparable_deals(subject) + find_valuation_comps(subject)

    # Step 3: Scrape the top deal pages and filings
    print(f"Scraping {min(len(results), max_pages)} deal pages...")
    deal_pages = []
    seen = set()
    for result in results:
        url = result.get("url")
        if not url or url in seen:
            continue
        seen.add(url)
        page = scrape_deal_page(url)
        if page["content"]:
            deal_pages.append(page)
        if len(deal_pages) >= max_pages:
            break

    # Step 4: Recent M&A news
    print("Pulling recent M&A news...")
    news = get_ma_news(subject)

    # Step 5: Generate the brief
    print("Generating M&A brief...")
    return generate_ma_brief(subject, landscape, deal_pages, news, context)

def print_brief(brief: dict):
    if not brief:
        print("Could not generate brief.")
        return

    print(f"\n{'='*60}")
    print(f"M&A Brief: {brief.get('subject', 'Subject')}")
    print(f"Deal type: {brief.get('deal_type', '?')}")
    print(f"Data quality: {brief.get('data_quality', '?').upper()}")
    print(f"{'='*60}")

    print(f"\nStrategic Rationale:\n{brief.get('strategic_rationale', '')}")

    comps = brief.get("comparable_transactions", [])
    if comps:
        print("\nComparable Transactions:")
        for c in comps:
            print(f"  * {c}")

    print(f"\nTypical Multiples:\n{brief.get('typical_multiples', '')}")

    acquirers = brief.get("potential_acquirers", [])
    if acquirers:
        print("\nPotential Acquirers:")
        for a in acquirers:
            print(f"  > {a}")

    print(f"\nRegulatory Considerations:\n{brief.get('regulatory_considerations', '')}")

    activity = brief.get("recent_deal_activity", [])
    if activity:
        print("\nRecent Deal Activity:")
        for d in activity:
            print(f"  ! {d}")

    flags = brief.get("due_diligence_flags", [])
    if flags:
        print("\nDue Diligence Flags:")
        for f in flags:
            print(f"  ? {f}")

    print(f"\n{brief.get('disclaimer', '')}")

if __name__ == "__main__":
    import sys

    # Usage: python agent.py "cybersecurity" sector
    subject = sys.argv[1] if len(sys.argv) > 1 else "cybersecurity M&A multiples"
    subject_type = sys.argv[2] if len(sys.argv) > 2 else "sector"

    CONTEXT = {
        "subject_type": subject_type,
        "deal_size": "mid-market",
        "geography": "US",
    }

    brief = research_ma(subject, CONTEXT, max_pages=5)
    if brief:
        print_brief(brief)

9. Common research topics

10. Use cases

11. Extending the agent

12. Disclaimer

This agent retrieves publicly available M&A information from news sources, SEC filings, and public research. It is not financial advice and does not replace professional M&A advisory services — for informational purposes only. M&A transactions involve significant complexity. Always conduct proper due diligence and consult qualified financial advisors and legal counsel before making any investment or acquisition decisions.

13. Getting your API key

Grab a free Superhighway key at /pricing (1,000 calls/month, no credit card). For an agent that provisions its own access, skip the key entirely with x402: it pays $0.002 per call in USDC on Base — no signup, no key management. See the x402 pay-per-call guide for the wallet setup.

For related builds, the financial research agent uses the same four-endpoint pattern on companies and markets, and the competitor analysis agent applies it to your market rivals.