Build a supply chain research agent

Superhighway guides

Mapping a supply chain means stitching together market structure, supplier directories, freight indices, customs data, and a constant stream of disruption news — port congestion, tariff changes, shortages — then turning all of it into something a procurement or operations team can act on. This guide builds a Python agent that runs that loop and produces a structured supply chain brief. It chains all four Superhighway endpoints — /research for a multi-source synthesis of a commodity or trade route, /search to find supplier directories and freight data, /scrape to pull pricing, lead times, and capabilities off supplier pages, and /news for recent disruptions — then uses an LLM to emit a clear brief: market overview, key suppliers, pricing trends, supply risks, alternative sources, lead-time estimates, recent disruptions, recommended actions, and an overall risk level.

1. What you'll build

A Python agent that takes a commodity, supplier category, trade route, or supply chain topic and produces a structured 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 supply chain landscape

Start with /research. One call pulls multiple sources into a synthesis of the market — its structure, scale, key players, where production is concentrated, and the pricing and geopolitical dynamics — so the LLM has grounded context instead of guessing.

import requests, os, json

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

def research_supply_landscape(commodity: str, pages: int = 6) -> str:
    """Deep synthesis of market structure, key players, and pricing landscape."""
    r = requests.get(
        f"{BASE}/research",
        params={
            "q": f"{commodity} supply chain market suppliers pricing logistics",
            "pages": pages
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    data = r.json()
    return data.get("synthesis", data.get("markdown", ""))[:3000]

4. Find suppliers and trade data

Two narrow /search calls: one hunts for supplier directories, manufacturers, and pricing/lead-time pages — the people who actually make the thing — and one looks for trade data, freight indices, and shipping-cost sources that tell you how it moves.

def find_suppliers(commodity: str, region: str = "global") -> list[dict]:
    """Find supplier directories, manufacturers, pricing, and lead times."""
    r = requests.get(
        f"{BASE}/search",
        params={
            "q": f"{commodity} {region} suppliers manufacturers pricing lead time",
            "limit": 8
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("results", [])

def find_trade_data(commodity: str) -> list[dict]:
    """Find trade data, freight indices, and shipping cost sources."""
    r = requests.get(
        f"{BASE}/search",
        params={
            "q": f"{commodity} import export trade data freight index shipping cost",
            "limit": 5
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("results", [])

5. Scrape supplier and industry pages

/scrape turns each supplier or trade page into clean, LLM-ready markdown — pricing schedules, lead times, product specs, capabilities — so the LLM summarizes real content, not just a title.

def scrape_page(url: str) -> dict:
    """Scrape a supplier or industry page for pricing, lead times, and specs."""
    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 supply chain disruption news

/news surfaces the time-sensitive layer a market report can't give you — fresh port congestion, tariff changes, strikes, shortages, and commodity price moves.

def get_disruption_news(commodity: str) -> list[dict]:
    """Get recent disruptions — congestion, tariffs, shortages, delays."""
    r = requests.get(
        f"{BASE}/news",
        params={
            "q": f"{commodity} supply chain disruption shortage shipping delay tariff",
            "count": 6
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("articles", [])

7. Generate the supply chain brief with an LLM

Now hand everything to the LLM. The system prompt pins the output to a procurement/operations reader, forces every claim back to the sources, and asks it to flag stale data on fast-moving topics. The output is structured JSON so it slots straight into a sourcing doc, BOM tracker, or risk dashboard.

from openai import OpenAI

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

def generate_supply_brief(
    topic: str,
    landscape: str,
    suppliers: list[dict],
    trade_data: list[dict],
    news: list[dict],
    context: dict
) -> dict | None:
    """Generate a structured supply chain intelligence brief."""

    supplier_text = "\n".join(
        f"- {s['title'][:80]}: {s['content'][:400]}"
        for s in suppliers[:5]
        if s.get("content")
    )

    trade_text = "\n".join(
        f"- {t['title'][:80]}: {t['content'][:300]}"
        for t in trade_data[:3]
        if t.get("content")
    )

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

    commodity_type = context.get("commodity_type", "component")
    region = context.get("region", "global")
    industry = context.get("industry", "general")

    response = llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""You are a supply chain research analyst helping a procurement and operations team.
Write a clear, structured supply chain brief. Only report information supported by the provided sources.
Be concrete about suppliers, countries of origin, pricing direction, and lead times.
Assess risk soberly — do not overstate or understate. If the topic is fast-moving and the data may be stale, flag it."""
            },
            {
                "role": "user",
                "content": f"""Supply chain brief on: {topic}

Commodity type: {commodity_type} | Region focus: {region} | Industry: {industry}

Market Landscape:
{landscape[:2000]}

Suppliers & Trade Data Found:
{supplier_text}

Freight & Trade Data:
{trade_text}

Recent Disruption News:
{news_text}

Return JSON with:
- commodity_or_topic: string
- market_overview: string (2-3 sentences on market structure, scale, key dynamics)
- key_suppliers: list of 3-5 strings (major suppliers/manufacturers with country of origin)
- geographic_concentration: string (where production is concentrated + what that means for risk)
- pricing_trends: string (current pricing environment, recent moves, outlook)
- lead_time_estimates: string (typical lead times and what's affecting them)
- supply_risks: list of 3-4 strings (geopolitical, weather, capacity, regulatory risks)
- alternative_sources: list of 2-3 strings (alternative suppliers/regions if primary disrupted)
- recent_disruptions: list of 3 strings (from the news — congestion, tariffs, shortages, strikes)
- recommended_actions: list of 3-4 strings (what the procurement/ops team should do)
- risk_level: "low" | "medium" | "high" | "critical"
- data_freshness: "current" | "may-be-dated" (flag if the topic is fast-moving and data may be stale)"""
            }
        ],
        response_format={"type": "json_object"}
    )

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

8. Wire up the full pipeline

The orchestrator runs research, search, scrape, and news, then hands the lot to the LLM. A context dict tunes the brief to a commodity type, region, and industry.

def research_supply_chain(
    topic: str,
    context: dict | None = None,
    max_pages: int = 5
) -> dict | None:
    """
    Research a commodity, supplier category, trade route, or supply chain topic.

    topic: e.g. "lithium carbonate supply chain", "PCB manufacturers Taiwan",
           "Trans-Pacific shipping rates", "rare earth magnets China"
    context: {
        "commodity_type": "raw-material" | "component" | "finished-good" | "service",
        "region": "US" | "EU" | "APAC" | "global",
        "industry": "electronics" | "automotive" | "pharma" | "food" | "general"
    }
    """
    if context is None:
        context = {
            "commodity_type": "component",
            "region": "global",
            "industry": "general"
        }

    region = context.get("region", "global")
    print(f"Researching: {topic} ({region})")

    # Deep market research
    print("Synthesizing supply chain landscape...")
    landscape = research_supply_landscape(topic, pages=6)

    # Find suppliers and trade data
    print("Finding suppliers and trade data...")
    suppliers_raw = find_suppliers(topic, region)
    trade_raw = find_trade_data(topic)

    # Scrape key pages
    print(f"Scraping {min(len(suppliers_raw), max_pages)} supplier pages...")
    suppliers = []
    for result in suppliers_raw[:max_pages]:
        page = scrape_page(result["url"])
        if page["content"]:
            page["title"] = page["title"] or result.get("title", "")
            suppliers.append(page)

    trade_data = []
    for result in trade_raw[:2]:
        page = scrape_page(result["url"])
        if page["content"]:
            trade_data.append(page)

    # Recent disruption news
    print("Fetching recent disruptions...")
    news = get_disruption_news(topic)

    # Generate brief
    print("Generating supply chain brief...")
    return generate_supply_brief(
        topic, landscape, suppliers, trade_data, news, context
    )

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

    risk_marks = {"low": "[LOW]", "medium": "[MED]", "high": "[HIGH]", "critical": "[CRIT]"}
    risk = brief.get("risk_level", "medium")

    print(f"\n{'='*60}")
    print(f"Supply Chain Brief: {brief.get('commodity_or_topic', 'Topic')}")
    print(f"Risk Level: {risk_marks.get(risk, '[?]')} {risk.upper()}")
    if brief.get("data_freshness") == "may-be-dated":
        print("! Note: fast-moving topic — verify current data before acting.")
    print(f"{'='*60}")

    print(f"\nMarket Overview: {brief.get('market_overview', '')}\n")

    suppliers = brief.get("key_suppliers", [])
    if suppliers:
        print("Key Suppliers:")
        for s in suppliers:
            print(f"  * {s}")

    geo = brief.get("geographic_concentration", "")
    if geo:
        print(f"\nGeographic Concentration: {geo}")

    pricing = brief.get("pricing_trends", "")
    if pricing:
        print(f"\nPricing Trends: {pricing}")

    lead = brief.get("lead_time_estimates", "")
    if lead:
        print(f"\nLead Times: {lead}")

    risks = brief.get("supply_risks", [])
    if risks:
        print("\nSupply Risks:")
        for r in risks:
            print(f"  ! {r}")

    alts = brief.get("alternative_sources", [])
    if alts:
        print("\nAlternative Sources:")
        for a in alts:
            print(f"  -> {a}")

    disruptions = brief.get("recent_disruptions", [])
    if disruptions:
        print("\nRecent Disruptions:")
        for d in disruptions:
            print(f"  > {d}")

    actions = brief.get("recommended_actions", [])
    if actions:
        print("\nRecommended Actions:")
        for a in actions:
            print(f"  [ ] {a}")

if __name__ == "__main__":
    import sys
    topic = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "lithium carbonate supply chain"

    CONTEXT = {
        "commodity_type": "raw-material",
        "region": "global",
        "industry": "automotive"
    }

    brief = research_supply_chain(topic, CONTEXT, max_pages=5)
    if brief:
        print_brief(brief)

9. Common topics to research

10. Use cases

11. Extending the agent

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 financial research agent uses the same four-endpoint pattern on companies and markets, and the regulatory research agent covers compliance and trade rules.