Build a financial research agent

Superhighway guides

Researching a stock means juggling a dozen tabs: investor relations pages, analyst notes, earnings coverage, and a notepad to stitch it into a bull case, a bear case, and the metrics that matter. This guide builds a Python agent that runs the whole loop automatically. It chains all four Superhighway endpoints — /research for the company's business model and competitive position, /search for analyst reports and earnings previews, /scrape for metrics from financial pages, and /news for earnings, guidance, and analyst actions — then uses an LLM to emit a structured research note as JSON.

1. What you'll build

A Python financial research agent that takes a stock ticker or company name and produces a structured investment 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 company

Start with /research, which pulls multi-source background into one synthesis — what the company does, how it makes money, its revenue segments, and where it sits competitively. This is the context that grounds every later step.

import requests, os, json

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

def research_company(company: str, ticker: str) -> str:
    """Deep company research: business model, revenue streams, competitive moat."""
    r = requests.get(
        f"{BASE}/research",
        params={
            "q": f"{company} {ticker} business model revenue segments competitive position strategy",
            "pages": 5
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    data = r.json()
    return data.get("synthesis", data.get("markdown", ""))[:3000]

4. Find analyst reports and earnings info

/search turns the ticker into a list of analyst notes, earnings previews, and valuation pieces — the candidate pages the agent will scrape for hard numbers.

def find_analyst_coverage(company: str, ticker: str) -> list[dict]:
    """Search for analyst reports, earnings previews, and financial analysis."""
    r = requests.get(
        f"{BASE}/search",
        params={
            "q": f"{ticker} {company} analyst report earnings forecast valuation",
            "limit": 6
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("results", [])

5. Scrape financial data pages

/scrape turns each candidate URL into clean, LLM-ready markdown. This is where the agent pulls actual metrics — revenue, growth rates, margins — out of investor relations and financial news pages.

def scrape_financial_page(url: str) -> dict:
    """Scrape a financial news or IR page for metrics and analysis."""
    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 market news

/news surfaces what just happened — earnings beats and misses, guidance changes, and analyst upgrades or downgrades. This is the time-sensitive layer that a static research note misses.

def get_market_news(company: str, ticker: str) -> list[dict]:
    """Get recent earnings, guidance, analyst upgrades/downgrades."""
    r = requests.get(
        f"{BASE}/news",
        params={
            "q": f"{ticker} {company} earnings revenue guidance analyst",
            "count": 6
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("articles", [])

7. Generate the investment brief with an LLM

Now hand everything to the LLM. The system prompt forbids inventing metrics and forbids buy/sell recommendations — the agent summarizes what's in the sources, it does not give advice. The output is structured JSON so it slots straight into a report or a dashboard.

from openai import OpenAI

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

def generate_brief(
    company: str,
    ticker: str,
    company_research: str,
    analyst_pages: list[dict],
    news: list[dict],
    focus: str = "long-term investor"
) -> dict | None:
    """Generate a structured investment research note."""

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

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

    response = llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""You are a financial analyst writing a research note for a {focus}.
Be specific and factual. Only use information from the provided sources.
Do not invent financial metrics — if a number isn't in the sources, say 'not found in sources.'
Do not make buy/sell recommendations."""
            },
            {
                "role": "user",
                "content": f"""Write a research brief for {company} ({ticker}).

Company Research:
{company_research[:2000]}

Analyst Coverage & Reports:
{analyst_text}

Recent News:
{news_text}

Return JSON with:
- company: string
- ticker: string
- business_summary: string (2-3 sentences on what they do and how they make money)
- key_metrics: list of strings (revenue, growth rate, margins — only what's in sources, labeled "not found" if absent)
- bull_case: list of 3 strings (strongest arguments for the company)
- bear_case: list of 3 strings (key risks and challenges)
- recent_developments: list of 2-3 strings (from the news — earnings beats/misses, guidance changes, analyst actions)
- investment_thesis: string (1 paragraph — the core thesis a long-term investor would hold)
- key_risks: list of 2-3 strings
- data_quality: "high" | "medium" | "low" (how much source material was found)"""
            }
        ],
        response_format={"type": "json_object"}
    )

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

8. The full research pipeline

Wire the steps together: research the company, find coverage, scrape the top pages, pull news, then generate the brief. The focus argument lets you slant the note for a long-term investor, a value investor, or a trader.

def research_stock(
    company: str,
    ticker: str,
    focus: str = "long-term investor",
    max_pages: int = 5
) -> dict | None:
    """
    Run the full research pipeline for a company.

    focus: "long-term investor" | "growth investor" | "value investor" | "trader"
    """
    print(f"Researching: {company} ({ticker})")

    # Step 1: Company background
    print("Researching company background...")
    company_research = research_company(company, ticker)

    # Step 2: Find analyst coverage
    print("Finding analyst coverage...")
    results = find_analyst_coverage(company, ticker)

    # Step 3: Scrape key pages
    print(f"Scraping {min(len(results), max_pages)} financial pages...")
    analyst_pages = []
    for result in results[:max_pages]:
        page = scrape_financial_page(result["url"])
        if page["content"]:
            analyst_pages.append(page)

    # Step 4: Recent news
    print("Pulling market news...")
    news = get_market_news(company, ticker)

    # Step 5: Generate brief
    print("Generating investment brief...")
    return generate_brief(company, ticker, company_research, analyst_pages, news, focus)

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

    print(f"\n{'='*60}")
    print(f"{brief.get('company', 'Company')} ({brief.get('ticker', '?')}) — Research Note")
    print(f"Data quality: {brief.get('data_quality', '?').upper()}")
    print(f"{'='*60}")
    print(f"\n{brief.get('business_summary', '')}\n")

    metrics = brief.get("key_metrics", [])
    if metrics:
        print("Key Metrics:")
        for m in metrics:
            print(f"  {m}")

    print("\nBull Case:")
    for bull in brief.get("bull_case", []):
        print(f"  + {bull}")

    print("\nBear Case:")
    for bear in brief.get("bear_case", []):
        print(f"  - {bear}")

    developments = brief.get("recent_developments", [])
    if developments:
        print("\nRecent Developments:")
        for d in developments:
            print(f"  * {d}")

    print(f"\nInvestment Thesis:\n{brief.get('investment_thesis', '')}")

    risks = brief.get("key_risks", [])
    if risks:
        print("\nKey Risks:")
        for risk in risks:
            print(f"  ! {risk}")

if __name__ == "__main__":
    import sys

    # Usage: python agent.py "Apple" AAPL
    if len(sys.argv) >= 3:
        company_name = sys.argv[1]
        ticker_symbol = sys.argv[2]
        investor_focus = sys.argv[3] if len(sys.argv) > 3 else "long-term investor"
    else:
        company_name = "Microsoft"
        ticker_symbol = "MSFT"
        investor_focus = "long-term investor"

    brief = research_stock(company_name, ticker_symbol, focus=investor_focus)
    if brief:
        print_brief(brief)

9. What you can research

10. Extending the agent

11. Important disclaimer

This agent retrieves and summarizes publicly available information. It does not provide investment advice. Always verify key metrics against official filings (10-K, 10-Q) before making investment decisions.

12. 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 competitor analysis agent uses the same four-endpoint pattern on your market rivals, and the content research agent covers the deep-research synthesis pattern in more depth.