Build a travel planning agent

Superhighway guides

Planning a trip means juggling a dozen browser tabs: destination guides, attraction sites for hours and prices, news for advisories and deals, and a notepad to stitch it all into a daily schedule. This guide builds a Python agent that does the whole loop automatically. It chains all four Superhighway endpoints — /research for deep destination knowledge, /search for attractions and restaurants, /scrape for hours and prices, and /news for current conditions — then uses an LLM to emit a structured, day-by-day itinerary as JSON.

1. What you'll build

A Python travel planning agent that:

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 destination

Start with /research, which pulls multi-source background into one synthesis — culture, getting around, safety, and which neighborhoods to base yourself in. 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_destination(destination: str) -> str:
    """Deep destination research: culture, transport, safety, best areas, local tips."""
    r = requests.get(
        f"{BASE}/research",
        params={
            "q": f"{destination} travel guide best areas transport safety culture tips",
            "pages": 5
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    data = r.json()
    return data.get("synthesis", data.get("markdown", ""))[:3000]

4. Find top attractions and experiences

/search turns the traveler's interests into a list of candidate attractions, restaurants, and experiences — the building blocks of the itinerary.

def find_attractions(destination: str, interests: list[str]) -> list[dict]:
    """Search for top attractions matching traveler interests."""
    interest_str = " ".join(interests) if interests else "top attractions"
    r = requests.get(
        f"{BASE}/search",
        params={
            "q": f"best {interest_str} {destination} travel",
            "limit": 8
        },
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("results", [])

5. Scrape attraction details

/scrape returns each travel page as clean Markdown — no nav, ads, or cookie banners — so the LLM sees opening hours, ticket prices, and visitor tips, not boilerplate. Truncate each so the prompt stays small.

def scrape_attraction(url: str) -> dict:
    """Scrape a travel page for hours, prices, and tips."""
    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", "")[:2000],
    }

6. Get travel news and current conditions

/news surfaces what a static guide never can: travel advisories, flight and hotel deals, and local events happening during the trip window.

def get_travel_news(destination: str) -> list[dict]:
    """Get current travel advisories, deals, and local events."""
    r = requests.get(
        f"{BASE}/news",
        params={"q": f"{destination} travel 2025", "count": 5},
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
    )
    return r.json().get("articles", [])

7. Generate the itinerary with an LLM

Now the LLM reads the destination research, the scraped attractions, and the news, then emits a structured day-by-day itinerary. A JSON response format keeps the output programmatic — ready to render, export, or feed into a booking step.

from openai import OpenAI

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

def generate_itinerary(
    destination: str,
    destination_research: str,
    attractions: list[dict],
    news: list[dict],
    trip_params: dict
) -> dict | None:
    """Generate a structured day-by-day itinerary."""

    attraction_summaries = "\n".join(
        f"- {a.get('title', a['url'])[:80]}: {a.get('content', '')[:300]}"
        for a in attractions[:6]
    )

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

    duration = trip_params.get("days", 5)
    budget = trip_params.get("budget", "mid-range")
    interests = ", ".join(trip_params.get("interests", ["culture", "food", "sightseeing"]))

    response = llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""You are an expert travel planner. Create practical, specific itineraries.

Trip details:
- Destination: {destination}
- Duration: {duration} days
- Budget: {budget}
- Interests: {interests}

Rules: Be specific with place names and timing. Include practical tips.
Only recommend what's in the provided research."""
            },
            {
                "role": "user",
                "content": f"""Plan a {duration}-day trip to {destination}.

Destination Research:
{destination_research[:2000]}

Discovered Attractions & Experiences:
{attraction_summaries}

Current Travel News:
{news_text}

Return JSON with:
- destination: string
- duration_days: number
- best_time_to_visit: string
- getting_there: string (1-2 sentences)
- getting_around: string (1-2 sentences)
- budget_estimate: string (per day estimate for {budget} budget)
- days: list of objects, each with:
  - day: number
  - theme: string (e.g. "Old Town & History")
  - morning: string
  - afternoon: string
  - evening: string
  - tips: string
- essential_tips: list of 3-4 strings
- current_notes: string (from the news — any advisories or deals)"""
            }
        ],
        response_format={"type": "json_object"}
    )

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

8. The full planning pipeline

Wire the steps together: research the destination, find and scrape attractions, pull news, then generate. The print_itinerary() helper renders the JSON into a readable trip plan.

def plan_trip(
    destination: str,
    trip_params: dict,
    max_attractions: int = 6
) -> dict | None:
    """
    trip_params: {
        "days": 7,
        "budget": "mid-range",    # budget | mid-range | luxury
        "interests": ["food", "art", "history", "nightlife"],
    }
    """
    print(f"Planning trip to: {destination}")

    # Research destination
    print("Researching destination...")
    destination_research = research_destination(destination)

    # Find attractions
    print("Finding attractions...")
    results = find_attractions(destination, trip_params.get("interests", []))

    # Scrape top results for details
    attractions = []
    for result in results[:max_attractions]:
        details = scrape_attraction(result["url"])
        if details["content"]:
            attractions.append(details)

    # Get travel news
    print("Getting current travel news...")
    news = get_travel_news(destination)

    # Generate itinerary
    print("Generating itinerary...")
    itinerary = generate_itinerary(
        destination,
        destination_research,
        attractions,
        news,
        trip_params
    )

    return itinerary

def print_itinerary(itinerary: dict):
    if not itinerary:
        print("Could not generate itinerary.")
        return

    print(f"\n{'='*60}")
    print(f"{itinerary.get('destination', 'Trip')} — {itinerary.get('duration_days', '?')}-Day Itinerary")
    print(f"{'='*60}")
    print(f"Best time to visit: {itinerary.get('best_time_to_visit', '')}")
    print(f"Getting there: {itinerary.get('getting_there', '')}")
    print(f"Getting around: {itinerary.get('getting_around', '')}")
    print(f"Daily budget: {itinerary.get('budget_estimate', '')}")

    if itinerary.get("current_notes"):
        print(f"\nCurrent: {itinerary['current_notes']}")

    print()
    for day in itinerary.get("days", []):
        print(f"Day {day.get('day', '?')}: {day.get('theme', '')}")
        print(f"  Morning:   {day.get('morning', '')[:100]}")
        print(f"  Afternoon: {day.get('afternoon', '')[:100]}")
        print(f"  Evening:   {day.get('evening', '')[:100]}")
        if day.get("tips"):
            print(f"  Tip: {day['tips'][:80]}")
        print()

    tips = itinerary.get("essential_tips", [])
    if tips:
        print("Essential tips:")
        for tip in tips:
            print(f"  - {tip}")

if __name__ == "__main__":
    import sys
    destination = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Lisbon Portugal"

    TRIP = {
        "days": 5,
        "budget": "mid-range",
        "interests": ["food", "history", "architecture", "local neighborhoods"],
    }

    itinerary = plan_trip(destination, TRIP, max_attractions=6)
    if itinerary:
        print_itinerary(itinerary)

9. What you can plan

10. Extending the agent

11. 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 lead generation agent applies the same search-scrape-score chain to sales prospects, and the competitor analysis agent uses the same four-endpoint pattern on your market rivals.