Build a travel planning agent
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:
- Takes a destination, travel dates, budget tier, and a list of interests
- Researches the destination deeply: culture, transport, safety, best areas to stay
- Finds and scrapes top attractions, restaurants, and accommodation options
- Gets current travel news — advisories, deal alerts, local events
- Uses an LLM to generate a structured day-by-day itinerary with JSON output
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
- City breaks — 3-5 day urban itineraries with neighborhood-by-neighborhood scheduling.
- Multi-city routes — run
plan_trip()for each city and combine into a regional route. - Interest-specific trips — pass
["surfing", "beaches"]or["museums", "galleries"]to skew the discovered results. - Budget tracking — the
budget_estimatefield helps validate whether a destination fits your spend target before booking.
10. Extending the agent
- Hotel search — add a
/searchcall forf"best hotels {destination} {budget}"and scrape booking pages for prices. - Flight monitoring — combine with a
/newscall forf"{origin} to {destination} flight deals"to catch sales. - Local event integration — pass a specific date and add
/news?q={destination} events {month} {year}to surface concerts, festivals, and markets. - PDF export — pipe
print_itinerary()output to a PDF library (reportlab, weasyprint) for a shareable trip doc.
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.