Production-ready American Airlines flight scraper with advanced bot evasion, automatic cookie management, and high-performance bulk scraping.
Scrapes both Award and Revenue flights from American Airlines with automatic cookie extraction, HTTP/2 support, circuit breaker pattern, and adaptive rate limiting. Now with concurrent bulk scraping for multiple routes and dates!
The easiest way to use this scraper is via Docker. No local setup required!
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15
Scrape multiple destinations and dates simultaneously!
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origins LAX SFO \
--destinations JFK MIA ORD DFW \
--dates 2025-12-15 2025-12-16 2025-12-17 \
--max-concurrent 5
This creates 2 Γ 4 Γ 3 = 24 route/date combinations that run concurrently! π
Key benefits:
- β‘ 10-20x faster than sequential scraping
- π Shared cookie management across all tasks
- π‘οΈ Built-in rate limiting and error isolation
- π Progress tracking for each combination
- π― One failed route doesn't stop others
That's it! The scraper will:
- β Automatically extract cookies using a real browser (Camoufox/Firefox)
- β Search for both Award and Revenue flights across all combinations
- β
Save results to
./output/ - β
Log everything to
./logs/ - β
Cache cookies in
./cookies/for reuse
================================================================================
π BULK CONCURRENT SCRAPING MODE
================================================================================
Origins: LAX, SFO
Destinations: JFK, MIA, ORD, DFW
Dates: 2025-12-15, 2025-12-16, 2025-12-17
Total combos: 24
Max concurrent: 10
Search types: Award, Revenue
================================================================================
π Starting: LAX β JFK on 2025-12-15
π Starting: LAX β MIA on 2025-12-15
...
β
Completed: LAX β JFK on 2025-12-15 (2/2 searches, 45 flights)
β
Completed: SFO β ORD on 2025-12-16 (2/2 searches, 38 flights)
...
================================================================================
β
BULK SCRAPING COMPLETE
================================================================================
Total combinations: 24
Successful: 23
Failed: 1
Duration: 45.2s
Avg per combo: 1.9s
================================================================================
Search for flights on a specific route and date:
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin SFO \
--destination LAX \
--date 2025-12-20
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destinations JFK MIA ORD DFW ATL \
--date 2025-12-15 \
--max-concurrent 5
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--dates 2025-12-15 2025-12-16 2025-12-17 2025-12-18 2025-12-19 \
--max-concurrent 5
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origins LAX SFO SEA \
--destinations JFK BOS PHL DCA \
--dates 2025-12-15 2025-12-20 2025-12-25 \
--max-concurrent 5
I recommend that instead of increasing the concurrency, you instead increase the number of cookies extracted, simulating the requests of three different browsers! Much safer from my tests and less likely to get blocked, whilst still achieving high net concurrency.
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origins LAX SFO SEA \
--destinations JFK BOS PHL DCA \
--dates 2025-12-15 2025-12-20 2025-12-25 \
--max-concurrent 5 \
--browsers 3
This creates 3 Γ 4 Γ 3 = 36 combinations!
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origins LAX SFO \
--destinations JFK MIA ORD \
--dates 2025-12-15 2025-12-16 \
--max-concurrent 5 \
--verbose
Performance recommendations:
- Safest:
--max-concurrent 5(This is the default option, totally safe) - Conservative:
--max-concurrent 5+--browsers 3(pretty safe, haven't tested scraping continuously for more than 5 minutes though) - Balanced:
--max-concurrent 10+--browsers 3(risky, likely to get blocked after a few minutes) - Aggressive:
--max-concurrent 15+--browsers 3(fast, will likely be blocked if scraping for more than 1 minute)
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin JFK \
--destination LHR \
--date 2025-12-25 \
--cabin BUSINESS
Available cabin classes: COACH, BUSINESS, FIRST, PREMIUM_ECONOMY
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin ORD \
--destination DFW \
--date 2025-12-15 \
--passengers 2
We can only search one of the API endpoints, I don't recommend doing so as that defeats the purpose of the data processing pipeline, but you can do so:
# Award flights only
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15 \
--search-type Award
# Revenue flights only
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15 \
--search-type Revenue
Watch the cookie extraction process in real-time:
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
-e DISPLAY=$DISPLAY \
-v /tmp/.X11-unix:/tmp/.X11-unix \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15 \
--no-headless
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15 \
--verbose
After running, you'll find:
output/
βββ LAX_JFK_2025-12-15_20251106_010602_combined.json # β
Merged Award + Revenue data
βββ SFO_MIA_2025-12-16_20251106_010605_combined.json # Another route/date combo
βββ raw_data/
βββ LAX_JFK_2025-12-15_20251106_010602_award_raw.json # Raw Award API response
βββ LAX_JFK_2025-12-15_20251106_010602_revenue_raw.json # Raw Revenue API response
βββ SFO_MIA_2025-12-16_20251106_010605_award_raw.json
βββ SFO_MIA_2025-12-16_20251106_010605_revenue_raw.json
cookies/
βββ aa_cookies.json # Extracted cookies (auto-cached)
βββ aa_cookies_headers.json # Request headers
βββ aa_cookies_referer.txt # Referer URL
logs/
βββ aa_scraper.log # Detailed logs with timestamps
The *_combined.json file contains merged Award + Revenue data:
{
"search_metadata": {
"origin": "LAX",
"destination": "JFK",
"date": "2025-12-15",
"passengers": 1,
"cabin_class": "economy"
},
"flights": [
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA28",
"departure_time": "00:15",
"arrival_time": "08:29"
}
],
"total_duration": "5h 14m",
"points_required": 18000,
"cash_price_usd": 208.48,
"taxes_fees_usd": 5.6,
"cpp": 1.13
},
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA118",
"departure_time": "06:05",
"arrival_time": "14:10"
}
],
"total_duration": "5h 5m",
"points_required": 15000,
"cash_price_usd": 208.48,
"taxes_fees_usd": 5.6,
"cpp": 1.35
},
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA2",
"departure_time": "07:00",
"arrival_time": "15:32"
}
],
"total_duration": "5h 32m",
"points_required": 17500,
"cash_price_usd": 208.48,
"taxes_fees_usd": 5.6,
"cpp": 1.16
},
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA307",
"departure_time": "08:00",
"arrival_time": "16:28"
}
],
"total_duration": "5h 28m",
"points_required": 20000,
"cash_price_usd": 258.49,
"taxes_fees_usd": 5.6,
"cpp": 1.26
},
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA238",
"departure_time": "10:15",
"arrival_time": "18:42"
}
],
"total_duration": "5h 27m",
"points_required": 31000,
"cash_price_usd": 368.48,
"taxes_fees_usd": 5.6,
"cpp": 1.17
},
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA32",
"departure_time": "11:20",
"arrival_time": "19:45"
}
],
"total_duration": "5h 25m",
"points_required": 27000,
"cash_price_usd": 298.49,
"taxes_fees_usd": 5.6,
"cpp": 1.08
},
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA274",
"departure_time": "12:37",
"arrival_time": "21:00"
}
],
"total_duration": "5h 23m",
"points_required": 27500,
"cash_price_usd": 368.48,
"taxes_fees_usd": 5.6,
"cpp": 1.32
},
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA4",
"departure_time": "15:40",
"arrival_time": "23:49"
}
],
"total_duration": "5h 9m",
"points_required": 15000,
"cash_price_usd": 143.48,
"taxes_fees_usd": 5.6,
"cpp": 0.92
},
{
"is_nonstop": true,
"segments": [
{
"flight_number": "AA10",
"departure_time": "21:45",
"arrival_time": "06:00"
}
],
"total_duration": "5h 15m",
"points_required": 20000,
"cash_price_usd": 258.49,
"taxes_fees_usd": 5.6,
"cpp": 1.26
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA1956",
"departure_time": "00:45",
"arrival_time": "06:46"
},
{
"flight_number": "AA2848",
"departure_time": "08:20",
"arrival_time": "11:37"
}
],
"total_duration": "7h 52m",
"points_required": 15000,
"cash_price_usd": 226.19,
"taxes_fees_usd": 5.6,
"cpp": 1.47
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA1956",
"departure_time": "00:45",
"arrival_time": "06:46"
},
{
"flight_number": "AA961",
"departure_time": "10:06",
"arrival_time": "13:29"
}
],
"total_duration": "9h 44m",
"points_required": 15000,
"cash_price_usd": 226.19,
"taxes_fees_usd": 5.6,
"cpp": 1.47
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA2808",
"departure_time": "06:06",
"arrival_time": "13:46"
},
{
"flight_number": "AA3190",
"departure_time": "17:05",
"arrival_time": "19:00"
}
],
"total_duration": "9h 54m",
"points_required": 15000,
"cash_price_usd": 226.19,
"taxes_fees_usd": 5.6,
"cpp": 1.47
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA2068",
"departure_time": "06:08",
"arrival_time": "11:19"
},
{
"flight_number": "AA1654",
"departure_time": "12:39",
"arrival_time": "17:15"
}
],
"total_duration": "8h 7m",
"points_required": 15000,
"cash_price_usd": 226.19,
"taxes_fees_usd": 5.6,
"cpp": 1.47
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA2497",
"departure_time": "07:59",
"arrival_time": "15:55"
},
{
"flight_number": "AA4781",
"departure_time": "17:54",
"arrival_time": "19:17"
}
],
"total_duration": "8h 18m",
"points_required": 15000,
"cash_price_usd": 226.19,
"taxes_fees_usd": 5.6,
"cpp": 1.47
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA3176",
"departure_time": "18:50",
"arrival_time": "20:23"
},
{
"flight_number": "AA276",
"departure_time": "22:37",
"arrival_time": "07:00"
}
],
"total_duration": "9h 10m",
"points_required": 15000,
"cash_price_usd": 226.19,
"taxes_fees_usd": 5.6,
"cpp": 1.47
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA6260",
"departure_time": "20:00",
"arrival_time": "21:30"
},
{
"flight_number": "AA276",
"departure_time": "22:37",
"arrival_time": "07:00"
}
],
"total_duration": "8h 0m",
"points_required": 19000,
"cash_price_usd": 233.18,
"taxes_fees_usd": 5.6,
"cpp": 1.2
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA820",
"departure_time": "22:37",
"arrival_time": "06:12"
},
{
"flight_number": "AA3199",
"departure_time": "08:09",
"arrival_time": "10:00"
}
],
"total_duration": "8h 23m",
"points_required": 15000,
"cash_price_usd": 226.19,
"taxes_fees_usd": 5.6,
"cpp": 1.47
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA2715",
"departure_time": "23:59",
"arrival_time": "04:58"
},
{
"flight_number": "AA1554",
"departure_time": "07:01",
"arrival_time": "11:28"
}
],
"total_duration": "8h 29m",
"points_required": 15000,
"cash_price_usd": 226.19,
"taxes_fees_usd": 5.6,
"cpp": 1.47
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA6371",
"departure_time": "17:03",
"arrival_time": "18:30"
},
{
"flight_number": "AA276",
"departure_time": "22:37",
"arrival_time": "07:00"
}
],
"total_duration": "10h 57m",
"points_required": 20500,
"cash_price_usd": 327.97,
"taxes_fees_usd": 11.2,
"cpp": 1.55
},
{
"is_nonstop": false,
"segments": [
{
"flight_number": "AA2453",
"departure_time": "22:49",
"arrival_time": "07:14"
},
{
"flight_number": "AA4721",
"departure_time": "12:03",
"arrival_time": "13:29"
}
],
"total_duration": "11h 40m",
"points_required": 15000,
"cash_price_usd": 231.97,
"taxes_fees_usd": 11.2,
"cpp": 1.47
}
],
"total_results": 20
}
Key Fields:
points_required: Award miles neededcash_price_usd: Revenue ticket pricecpp: Cents per point (award value calculation)is_nonstop: Whether it's a direct flightsegments: Individual flight legs with times
Want to develop locally or run without Docker? Use uv - the modern Python package manager.
- Python 3.12
- uv (install:
curl -LsSf https://astral.sh/uv/install.sh | sh)
# Clone the repository
git clone https://github.com/yourusername/aa-scraper.git
cd aa-scraper
# Install dependencies with uv
uv sync
This creates a virtual environment and installs all dependencies (including Camoufox browser).
# Single route
uv run -m aa_scraper \
--origin LAX \
--destination JFK \
--date 2025-12-15
# Bulk scraping
uv run -m aa_scraper \
--origins LAX SFO \
--destinations JFK MIA ORD \
--dates 2025-12-15 2025-12-16 \
--max-concurrent 5
# Install dev dependencies
uv sync --all-extras
# Run tests
uv run pytest
# Format code
uv run black aa_scraper/
# Lint
uv run ruff check aa_scraper/
# Build Docker image locally
docker build -t aa-scraper:local .
- Concurrent Execution: Scrape 10-15+ routes simultaneously
- Matrix Combinations: Origins Γ Destinations Γ Dates
- Shared Cookie Pool: All tasks use same authentication
- Error Isolation: One failed route doesn't stop others
- Progress Tracking: Real-time status for each combination
- Smart Semaphore: Configurable concurrency limits
- Camoufox Browser: Uses Firefox-based browser with anti-detection
- Automatic Cookie Extraction: Real browser session with proper headers
- Akamai Bypass: Handles and solves Akamai bot challenges automatically
- HTTP/2 Support: Native HTTP/2 with fallback to HTTP/1.1
- Smart Header Ordering: Mimics real browser request patterns
- Circuit Breaker Pattern: Prevents cascading failures
- Exponential Backoff: Smart retry logic with jitter
- Rate Limiting: Adaptive rate limiter (1 req/s default)
- Cookie Auto-Refresh: Detects expired cookies and refreshes automatically
- Health Checks: Built-in monitoring and recovery
- Dual Search: Award + Revenue flights in one run
- CPP Calculation: Automatic cents-per-point value
- Flight Matching: Merges Award + Revenue for same flights
- Raw Data Saved: Full API responses preserved
- Structured Output: Clean JSON format
# Cookie directory (default: /app/cookies)
COOKIES_DIR=/path/to/cookies
# Output directory (default: /app/output)
OUTPUT_DIR=/path/to/output
# Log directory (default: /app/logs)
LOGS_DIR=/path/to/logs
# Browser path (auto-detected)
PLAYWRIGHT_BROWSERS_PATH=/path/to/browsers
Flight Search:
--origin ORIGIN Origin airport code
--destination DESTINATION Destination airport code
--date DATE Departure date YYYY-MM-DD
--origins ORIGIN [ORIGIN ...] Multiple origins for bulk search
--destinations DEST [DEST ...] Multiple destinations for bulk search
--dates DATE [DATE ...] Multiple dates for bulk search
--passengers N Number of passengers (default: 1)
--cabin CLASS Cabin class (default: COACH)
--search-type TYPE Award, Revenue, or both (default: both)
--max-concurrent N Max concurrent combinations (default: 10)
Cookie Management:
--cookies FILE Cookie file path
--no-headless Show browser during cookie extraction
--cookie-wait-time N Wait time for API response (default: 15s)
Configuration:
--output DIR Output directory (default: ./output)
--rate-limit N Requests per second (default: 1.0)
--verbose Enable debug logging
--log-file FILE Log file path
-
Cookie Extraction (automatic on first run)
- Launches Camoufox browser (real Firefox)
- Navigates to AA.com and accepts cookies
- Triggers a test flight search to warm up session
- Captures cookies, headers, and referer
- Validates API response before saving
-
Bulk Scraping Mode (when using
--origins,--destinations, or--dates)- Generates all origin Γ destination Γ date combinations
- Creates semaphore to limit concurrent tasks
- Launches tasks up to
--max-concurrentlimit - Each task independently searches Award + Revenue
- Shared cookie pool across all tasks
- Automatic error handling and retries per task
-
Flight Search (per route/date combination)
- Uses extracted cookies to make API requests
- Searches Award flights (miles + taxes)
- Searches Revenue flights (cash prices)
- Matches flights by time/route
- Calculates CPP (cents per point) value
-
Cookie Reuse
- Cookies cached for 30 minutes
- Auto-refresh when expired
- Retry logic on 403 errors
- Exponential backoff on rate limits
-
Data Merging
- Matches Award + Revenue flights by departure/arrival times
- Calculates true award value (CPP)
- Filters by requested cabin class
- Saves structured JSON output per combination
- Default: 1 request per second (safe)
- AA.com has bot detection - don't go too fast
- Circuit breaker opens after 3 consecutive failures
- Automatic backoff on 429 responses
- Bulk mode: Each concurrent task respects rate limit independently
- Start conservative:
--max-concurrent 5and--rate-limit 1.0 - Monitor logs: Watch for 403/429 errors
- Increase gradually: If successful, try 10-15 concurrent
- Cookie efficiency: All tasks share same cookies (good!)
- RAM usage: Each task ~100-200MB, so 10 concurrent β 1-2GB
- One cookie extraction: Browser only launches once, not per task
- Cookies expire after 30 minutes
- Auto-refresh triggers at 20 minutes
- Force refresh on 403 errors
- Test flight validates cookies work
- Automatically detected and solved
- May add 10-30s to first request
- Transparent handling - no user action needed
Always mount these directories:
/app/cookies- Cookie persistence/app/output- Results storage/app/logs- Log files
Without volumes, data is lost when container stops!
Solution 1: Increase wait time
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15 \
--cookie-wait-time 30
Solution 2: Run with visible browser to see what's happening
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
-e DISPLAY=$DISPLAY \
-v /tmp/.X11-unix:/tmp/.X11-unix \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15 \
--no-headless
This means cookies are invalid or flagged. The scraper auto-refreshes cookies, but you can force it:
# Delete cached cookies
rm -rf cookies/*
# Run again - fresh cookies will be extracted
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15
Too many failures detected. Wait 5 minutes or adjust rate limit:
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--date 2025-12-15 \
--rate-limit 0.5 # Slower = safer
Reduce concurrency or rate limit:
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origins LAX SFO \
--destinations JFK MIA ORD \
--dates 2025-12-15 2025-12-16 \
--max-concurrent 5 \
--rate-limit 0.8
# View live logs
tail -f logs/aa_scraper.log
# Search for errors
grep ERROR logs/aa_scraper.log
# Check successful completions
grep "β
Completed" logs/aa_scraper.log
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destinations JFK MIA ORD DFW ATL \
--date 2025-12-15 \
--search-type Award \
--max-concurrent 5
Result: Compare award availability across 5 destinations in ~10 seconds!
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destination JFK \
--dates 2025-12-15 2025-12-16 2025-12-17 2025-12-18 2025-12-19 2025-12-20 2025-12-21 \
--max-concurrent 7
Result: Check 7 days of availability in ~15 seconds!
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origins LAX SFO SEA \
--destinations JFK BOS PHL \
--date 2025-12-15 \
--search-type Award \
--cabin BUSINESS \
--max-concurrent 9
Result: 3Γ3 = 9 combinations, find best west-to-east coast business class award!
# Add to crontab: Check daily at 8am
0 8 * * * docker run --rm \
-v "$HOME/aa-scraper/cookies:/app/cookies" \
-v "$HOME/aa-scraper/output:/app/output" \
-v "$HOME/aa-scraper/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX \
--destinations JFK MIA \
--dates 2025-12-15 2025-12-16 \
--max-concurrent 4
# Scrape comprehensive dataset
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origins LAX SFO \
--destinations JFK MIA ORD \
--dates 2025-12-15 2025-12-20 2025-12-25 \
--max-concurrent 5
# Find best CPP values
grep -r "cpp" output/*_combined.json | \
jq -r '.flights[] | "$$.cpp) - $$.segments.departure_time) $$.segments[-1].arrival_time)"' | \
sort -n -r | head -20
aa_scraper/
βββ __init__.py # Package initialization
βββ __main__.py # Entry point
βββ cli.py # Command-line interface + bulk scraping
βββ config.py # Configuration constants
βββ api_client.py # HTTP client with retry logic
βββ cookie_manager.py # Cookie extraction & management
βββ parser.py # API response parser
βββ storage.py # Results persistence
βββ rate_limiter.py # Adaptive rate limiting
βββ circuit_breaker.py # Circuit breaker pattern
βββ retry.py # Exponential backoff
βββ exceptions.py # Custom exceptions
βββ logging_config.py # Loguru setup
βββ models.py # Data models & enums
Key Components:
- scrape_bulk_concurrent(): Orchestrates parallel route/date scraping
- CookieManager: Browser automation with Camoufox, handles Akamai
- AAFlightClient: HTTP/2 client with auto-recovery
- AdaptiveRateLimiter: Token bucket algorithm with backoff
- CircuitBreaker: Prevents cascading failures
- FlightDataParser: Extracts flight data from API JSON
Single Route Times:
- Cookie extraction: 15-30 seconds (first run only)
- Award search: 2-3 seconds (with cached cookies)
- Revenue search: 2-3 seconds
- Total (both): ~5-6 seconds after initial cookie setup
Bulk Scraping Performance:
- 10 routes concurrently: ~6-8 seconds total (vs 50-60s sequential)
- 24 combinations (2Γ4Γ3): ~12-15 seconds (vs 2-3 minutes sequential)
- 50 combinations (5Γ5Γ2): ~25-30 seconds (vs 4-5 minutes sequential)
- Speedup: 10-20x faster than sequential
Throughput:
- Default rate limit: 1 req/s per task (safe for production)
- Burst capacity: 2 requests per task
- Handles 429 rate limits automatically
- Recommended max concurrent: 10-15 tasks
Success Rate:
- Cookie extraction: >95% (with retries)
- API searches: >98% (with valid cookies)
- Akamai challenge pass rate: 100% (automated)
- Bulk scraping: >95% per combination (with error isolation)
Contributions welcome! This is a production-ready tool, so please:
- Test thoroughly before submitting PRs
- Follow existing patterns (circuit breaker, retry logic, etc.)
- Update tests for new features
- Document changes in README
MIT License - see LICENSE file
Docker - Single Route
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origin LAX --destination JFK --date 2025-12-15
Docker - Bulk Scraping
docker run --rm \
-v "$(pwd)/cookies:/app/cookies" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/logs:/app/logs" \
thermostatic/aa-scraper:latest \
--origins LAX SFO \
--destinations JFK MIA ORD \
--dates 2025-12-15 2025-12-16 \
--max-concurrent 5
Local (uv) - Single Route
uv run -m aa_scraper \
--origin LAX --destination JFK --date 2025-12-15
Local (uv) - Bulk Scraping
uv run -m aa_scraper \
--origins LAX SFO \
--destinations JFK MIA ORD \
--dates 2025-12-15 2025-12-16 \
--max-concurrent 5
Docker Hub π³ https://hub.docker.com/repository/docker/thermostatic/aa-scraper
Made with β€οΈ for award travel enthusiasts