How to Scrape Booking.com in 2026: Hotel Data, Prices, and Reviews

Booking.com holds one of the richest datasets in the travel industry — hotel listings, nightly rates, guest reviews, availability calendars, and property photos across millions of properties worldwide. Whether you’re building a price comparison tool, analyzing travel trends, or doing market research for the hospitality industry, Booking.com data is incredibly valuable.

But scraping it? That’s where things get interesting.

In this guide, I’ll walk you through scraping Booking.com hotel data with Python — what works, what doesn’t, and the anti-bot challenges you’ll face in 2026.

What Data Can You Extract from Booking.com?

Before writing any code, let’s map out what’s available:

  • Hotel listings — name, star rating, address, coordinates, property type
  • Pricing — nightly rates, total stay cost, taxes, discounts
  • Availability — room types, dates available, occupancy limits
  • Reviews — guest scores, review text, reviewer country, review date
  • Photos — property images, room photos
  • Amenities — WiFi, parking, breakfast, pool, etc.

The search results page is the easiest entry point. You search by location and dates, and Booking returns a paginated list of properties with pricing.

Step 1: Understanding the Search URL Structure

Booking.com search URLs follow a predictable pattern:

https://www.booking.com/searchresults.html?ss=Paris&checkin=2026-04-15&checkout=2026-04-18&group_adults=2&no_rooms=1

Key parameters:

  • ss — search query (city, region, or hotel name)
  • checkin / checkout — dates in YYYY-MM-DD format
  • group_adults — number of guests
  • no_rooms — number of rooms
  • offset — pagination (25 results per page, so offset=25 for page 2)

Step 2: Basic Scraper with requests + BeautifulSoup

Here’s a starting point that extracts hotel names and prices from search results:

import requests
from bs4 import BeautifulSoup
import json
import time
import random

def scrape_booking_search(location, checkin, checkout, pages=3):
    """Scrape Booking.com search results for hotel listings."""

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/124.0.0.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    }

    hotels = []

    for page in range(pages):
        offset = page * 25
        url = (
            f"https://www.booking.com/searchresults.html"
            f"?ss={location}&checkin={checkin}&checkout={checkout}"
            f"&group_adults=2&no_rooms=1&offset={offset}"
        )

        response = requests.get(url, headers=headers, timeout=30)

        if response.status_code != 200:
            print(f"Page {page + 1}: Got status {response.status_code}")
            continue

        soup = BeautifulSoup(response.text, "html.parser")

        # Booking.com uses data attributes on property cards
        property_cards = soup.select('[data-testid="property-card"]')

        for card in property_cards:
            hotel = {}

            # Hotel name
            title_el = card.select_one('[data-testid="title"]')
            hotel["name"] = title_el.get_text(strip=True) if title_el else None

            # Price
            price_el = card.select_one('[data-testid="price-and-discounted-price"]')
            hotel["price"] = price_el.get_text(strip=True) if price_el else None

            # Review score
            score_el = card.select_one('[data-testid="review-score"]')
            hotel["review_score"] = score_el.get_text(strip=True) if score_el else None

            # Link to property page
            link_el = card.select_one('a[data-testid="title-link"]')
            hotel["url"] = link_el["href"] if link_el else None

            # Address / distance
            distance_el = card.select_one('[data-testid="distance"]')
            hotel["distance"] = distance_el.get_text(strip=True) if distance_el else None

            if hotel["name"]:
                hotels.append(hotel)

        print(f"Page {page + 1}: Found {len(property_cards)} properties")

        # Be respectful — random delay between requests
        time.sleep(random.uniform(2, 5))

    return hotels

# Usage
results = scrape_booking_search("Paris", "2026-04-15", "2026-04-18")
print(f"Total hotels found: {len(results)}")

for hotel in results[:5]:
    print(f"  {hotel['name']} — {hotel['price']} — Score: {hotel['review_score']}")

Step 3: Extracting Detailed Hotel Data

Once you have property URLs, you can scrape individual hotel pages for deeper data:

def scrape_hotel_details(hotel_url):
    """Extract detailed information from a Booking.com hotel page."""

    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/124.0.0.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = requests.get(hotel_url, headers=headers, timeout=30)
    soup = BeautifulSoup(response.text, "html.parser")

    details = {}

    # Property name and type
    name_el = soup.select_one('h2.pp-header__title')
    details["name"] = name_el.get_text(strip=True) if name_el else None

    # Address
    address_el = soup.select_one('[data-node_tt_id="location_score_tooltip"]')
    details["address"] = address_el.get_text(strip=True) if address_el else None

    # Overall review score
    score_el = soup.select_one('[data-testid="review-score-component"]')
    details["score"] = score_el.get_text(strip=True) if score_el else None

    # Description
    desc_el = soup.select_one('[data-testid="property-description"]')
    details["description"] = desc_el.get_text(strip=True) if desc_el else None

    # Amenities / facilities
    amenities = []
    for item in soup.select('[data-testid="facility-group-icon"]'):
        text = item.find_parent().get_text(strip=True)
        if text:
            amenities.append(text)
    details["amenities"] = amenities

    # Try to extract structured data from JSON-LD
    for script in soup.select('script[type="application/ld+json"]'):
        try:
            ld_data = json.loads(script.string)
            if ld_data.get("@type") == "Hotel":
                details["star_rating"] = ld_data.get("starRating", {}).get("ratingValue")
                details["coordinates"] = {
                    "lat": ld_data.get("geo", {}).get("latitude"),
                    "lng": ld_data.get("geo", {}).get("longitude"),
                }
                details["aggregate_rating"] = ld_data.get("aggregateRating", {}).get("ratingValue")
        except (json.JSONDecodeError, TypeError):
            continue

    return details

The Anti-Bot Problem (This Is Where It Gets Hard)

Here’s the honest truth: Booking.com has some of the most aggressive anti-bot protection in the travel industry. In 2026, you’ll face:

  1. Akamai Bot Manager — sophisticated browser fingerprinting that detects headless browsers
  2. CAPTCHA challenges — triggered after just a few requests from datacenter IPs
  3. Rate limiting — aggressive throttling that returns 429 or redirects to CAPTCHA pages
  4. Dynamic rendering — prices and availability often load via JavaScript after the initial page load

With plain requests, you’ll get blocked within 10-20 requests from a datacenter IP. This is where residential proxies become essential.

Using Residential Proxies for Reliable Scraping

Residential proxies route your requests through real consumer IP addresses, making your traffic look like normal browsing. For Booking.com specifically, this is not optional — it’s required for any meaningful data collection.

ThorData offers residential proxies with geo-targeting, which is particularly useful for Booking.com since prices vary by the visitor’s location:

import requests

PROXY_URL = "http://YOUR_USER:[email protected]:9000"

proxies = {
    "http": PROXY_URL,
    "https": PROXY_URL,
}

def scrape_with_proxy(url, headers):
    """Make a request through residential proxy."""
    try:
        response = requests.get(
            url,
            headers=headers,
            proxies=proxies,
            timeout=30,
        )
        return response
    except requests.exceptions.ProxyError as e:
        print(f"Proxy error: {e}")
        return None

# Geo-target to see prices in a specific currency
# ThorData supports country-level targeting

Why geo-targeting matters for Booking.com: Hotels show different prices based on where you’re browsing from. A hotel in Paris might show €120/night to a French visitor but €135 to someone browsing from the US. If you’re doing price comparison, you need to control which country your requests come from.

Handling JavaScript-Rendered Content

Some pricing data on Booking.com loads dynamically. For those cases, you’ll need a browser automation tool:

from playwright.sync_api import sync_playwright
import json

def scrape_with_browser(location, checkin, checkout):
    """Use Playwright for JS-rendered Booking.com pages."""

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
            viewport={"width": 1920, "height": 1080},
            locale="en-US",
        )

        page = context.new_page()

        url = (
            f"https://www.booking.com/searchresults.html"
            f"?ss={location}&checkin={checkin}&checkout={checkout}"
            f"&group_adults=2&no_rooms=1"
        )

        page.goto(url, wait_until="networkidle")

        # Wait for price elements to render
        page.wait_for_selector('[data-testid="price-and-discounted-price"]', timeout=15000)

        # Extract data from the rendered page
        hotels = page.evaluate("""
            () => {
                const cards = document.querySelectorAll('[data-testid="property-card"]');
                return Array.from(cards).map(card => ({
                    name: card.querySelector('[data-testid="title"]')?.textContent?.trim(),
                    price: card.querySelector('[data-testid="price-and-discounted-price"]')?.textContent?.trim(),
                    score: card.querySelector('[data-testid="review-score"]')?.textContent?.trim(),
                }));
            }
        """)

        browser.close()
        return hotels

Extracting Review Data

Guest reviews are one of the most valuable parts of Booking.com data. Each property has a reviews page:

def scrape_reviews(property_id, pages=5):
    """Scrape guest reviews for a specific Booking.com property."""

    reviews = []

    for page in range(pages):
        offset = page * 25
        # Reviews are loaded via an internal API endpoint
        url = (
            f"https://www.booking.com/reviewlist.html"
            f"?pagename={property_id}&offset={offset}&rows=25"
        )

        response = requests.get(url, headers=headers, proxies=proxies, timeout=30)
        soup = BeautifulSoup(response.text, "html.parser")

        for review_el in soup.select('.review_item'):
            review = {}

            score = review_el.select_one('.review-score-badge')
            review["score"] = score.get_text(strip=True) if score else None

            title = review_el.select_one('.review_item_header_content')
            review["title"] = title.get_text(strip=True) if title else None

            positive = review_el.select_one('.review_pos')
            review["positive"] = positive.get_text(strip=True) if positive else None

            negative = review_el.select_one('.review_neg')
            review["negative"] = negative.get_text(strip=True) if negative else None

            date_el = review_el.select_one('.review_item_date')
            review["date"] = date_el.get_text(strip=True) if date_el else None

            reviews.append(review)

        time.sleep(random.uniform(2, 4))

    return reviews

Saving to CSV

import csv

def save_to_csv(hotels, filename="booking_hotels.csv"):
    """Save scraped hotel data to CSV."""
    if not hotels:
        return

    keys = hotels[0].keys()
    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=keys)
        writer.writeheader()
        writer.writerows(hotels)

    print(f"Saved {len(hotels)} hotels to {filename}")

Limitations and Honest Assessment

Let me be upfront about the challenges:

  1. Booking.com actively fights scraping. Their anti-bot is among the best. Expect to invest in residential proxies and browser fingerprint management.
  2. Prices are session-dependent. The same hotel can show different prices based on cookies, login status, and browsing history. Getting accurate price data requires careful session management.
  3. Selectors change frequently. Booking.com updates their frontend regularly. Your selectors will break — budget time for maintenance.
  4. Scale is expensive. Between proxy costs and the slow pace required to avoid detection, scraping thousands of properties daily requires real infrastructure investment.
  5. Legal considerations. Booking.com’s ToS prohibit automated scraping. Use the data responsibly, respect rate limits, and consider whether their affiliate API might meet your needs instead.

When to Use the Booking.com Affiliate API Instead

Before building a scraper, check if the Booking.com Affiliate Partner API gives you what you need. It provides:

  • Hotel search and availability
  • Pricing data
  • Property details and photos

The API is free for affiliates and doesn’t require proxy infrastructure. The trade-off is that you’re limited to their data format and rate limits, and you need to apply for partner access.

Summary

Scraping Booking.com is doable but challenging. For small-scale research (a few hundred properties), the approach above works. For production-scale data collection, you’ll need residential proxies, browser automation, and a maintenance plan for when selectors inevitably break.

The key insight: start with their API if it meets your needs. Only build a scraper if you need data the API doesn’t provide — like competitor pricing from the guest perspective, or historical price trends.

Happy scraping, and remember: always be respectful of the sites you scrape. Rate-limit your requests and don’t hammer their servers.

How I Built an AI Job Board That Auto-Updates from 164+ Companies

Нефункциональные требования

The Problem

If you’re looking for an AI engineering role in 2026, you’re probably checking 10-20 individual career pages. Anthropic’s Greenhouse page, OpenAI’s careers site, DeepMind’s jobs board, Cohere’s Lever page…

General job boards like LinkedIn and Indeed have AI roles, but the filtering is terrible. Search “AI engineer” and you get results for “AI-powered customer service” and “engineer at an AI startup doing nothing related to AI.”

I wanted a single page with every AI/ML/LLM role from every major AI company, updated automatically.

So I built LLMHire.

Architecture

Stack: Next.js 14 (App Router) + Supabase (PostgreSQL) + Vercel (hosting + crons)

The core idea is simple: most tech companies use one of three ATS platforms — Greenhouse, Ashby, or Lever. Each exposes a public API for their job listings. I built adapters for each.

The Fetcher Pattern

Each ATS has a fetcher module:

  • Greenhouse: GET https://boards-api.greenhouse.io/v1/boards/{company}/jobs
  • Ashby: POST https://jobs.ashbyhq.com/api/non-auth-job-posting
  • Lever: GET https://api.lever.co/v0/postings/{company}

Each fetcher normalizes the response into a common schema:

  • title, company, location, department
  • remote/hybrid/onsite classification
  • role type (ML Engineer, LLM Engineer, Research Scientist, etc.)
  • posting date, description, apply URL

Auto-Categorization

Job title → role type mapping uses pattern matching:

  • Contains “LLM” or “NLP” or “language model” → LLM Engineer
  • Contains “ML” or “machine learning” → ML Engineer
  • Contains “research” + “scientist” → AI Research
  • Contains “infrastructure” or “platform” → AI Infrastructure
  • Contains “prompt” → Prompt Engineer

This gets ~90% accuracy. Edge cases get manually reviewed.

Cron Jobs

Vercel cron jobs run every 4 hours (6x/day):

  1. Fetch all jobs from all companies
  2. Deduplicate against existing listings (by company + title + location hash)
  3. Mark stale listings (>30 days without refresh) for review
  4. Update search index

The Numbers

  • 130+ companies tracked
  • 670+ active positions
  • ~50-100 new/updated listings per day
  • Average fetch cycle: ~45 seconds for all companies

What the Data Shows

After tracking the AI job market for a few weeks:

  1. “LLM Engineer” is a real role now — distinct from ML Engineer, with different skill requirements
  2. Remote is declining — major labs want hybrid/onsite
  3. AI Infrastructure is booming — fastest growing subcategory
  4. Prompt Engineering peaked — being absorbed into broader roles

What’s Next

  • Workable integration (another ATS with significant AI company coverage)
  • Salary data aggregation
  • Weekly AI jobs newsletter
  • Blog with hiring market analysis

Try It

LLMHire is completely free. No accounts, no fees, no signup required to browse. If you have feedback or companies I should add, drop a comment.

Open-Meteo Has a Free API — Get Weather Data for Any Location Without an API Key

Weather data is one of the most common API needs — from dashboards to travel apps to IoT projects. But most weather APIs require registration, API keys, and have restrictive free tiers.

Open-Meteo is completely free, open-source, and requires no API key. Just send a request and get weather data.

What You Get

One HTTP request returns:

  • Current temperature, wind speed, wind direction
  • Hourly forecasts (up to 16 days)
  • Daily min/max temperature, precipitation, sunrise/sunset
  • Historical weather data (back to 1940)
  • Air quality index
  • Marine and flood forecasts

Try It Right Now

curl "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true"

Response:

{
  "latitude": 52.52,
  "longitude": 13.42,
  "current_weather": {
    "temperature": 15.3,
    "windspeed": 12.4,
    "winddirection": 210,
    "weathercode": 3,
    "is_day": 1,
    "time": "2026-03-26T10:00"
  }
}

That’s Berlin right now — 15.3°C with moderate wind. No key needed.

Python Example: 7-Day Forecast

import requests

def get_forecast(city_lat, city_lon, city_name=""):
    url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": city_lat,
        "longitude": city_lon,
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
        "timezone": "auto",
        "forecast_days": 7
    }
    response = requests.get(url, params=params)
    data = response.json()

    print(f"7-Day Forecast for {city_name} ({city_lat}, {city_lon})")
    print("-" * 50)

    for i, date in enumerate(data["daily"]["time"]):
        max_t = data["daily"]["temperature_2m_max"][i]
        min_t = data["daily"]["temperature_2m_min"][i]
        rain = data["daily"]["precipitation_sum"][i]
        print(f"{date}: {min_t}°C — {max_t}°C | Rain: {rain}mm")

# Try it
get_forecast(48.8566, 2.3522, "Paris")
get_forecast(40.7128, -74.0060, "New York")
get_forecast(35.6762, 139.6503, "Tokyo")

JavaScript (Node.js) Example

async function getWeather(lat, lon, city) {
  const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&hourly=temperature_2m&timezone=auto`;

  const res = await fetch(url);
  const data = await res.json();

  const current = data.current_weather;
  console.log(`${city}: ${current.temperature}°C, wind ${current.windspeed} km/h`);

  // Next 6 hours
  const now = new Date();
  const hourIndex = now.getHours();
  const next6 = data.hourly.temperature_2m.slice(hourIndex, hourIndex + 6);
  console.log(`Next 6 hours: ${next6.map(t => t + "°C").join(", ")}`);
}

getWeather(51.5074, -0.1278, "London");

Historical Weather Data (Back to 1940!)

This is where Open-Meteo really shines. Most APIs charge premium for historical data:

import requests

def historical_weather(lat, lon, start, end):
    url = "https://archive-api.open-meteo.com/v1/archive"
    params = {
        "latitude": lat,
        "longitude": lon,
        "start_date": start,
        "end_date": end,
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
        "timezone": "auto"
    }
    response = requests.get(url, params=params)
    return response.json()

# What was the weather in London on D-Day?
data = historical_weather(51.5074, -0.1278, "1944-06-06", "1944-06-06")
print(data["daily"])
# Real historical weather data — no API key, completely free

Air Quality Index

curl "https://air-quality-api.open-meteo.com/v1/air-quality?latitude=52.52&longitude=13.41&current=pm10,pm2_5,carbon_monoxide,nitrogen_dioxide"

Get real-time air quality data for any location — PM2.5, PM10, CO, NO2 levels.

Compared to Alternatives

Service Free Tier Key Required Historical Data Open Source
Open-Meteo 10K req/day No Yes (1940+) Yes
OpenWeatherMap 1K req/day Yes Paid No
WeatherAPI 1M req/month Yes Paid No
Visual Crossing 1K req/day Yes Yes No
Tomorrow.io 500 req/day Yes No No

Open-Meteo wins on every metric: more requests, no key, free historical data, AND it’s open source.

Real-World Use Cases

1. Travel planning app — A developer I know built a trip planner that shows weather forecasts for each destination. Open-Meteo made it possible without any API costs.

2. Agricultural monitoring — Farmers use historical precipitation data to plan irrigation schedules. 80+ years of rainfall data, completely free.

3. Smart home dashboards — Display current weather on your Raspberry Pi dashboard. One fetch() call, no token management.

4. Climate research — Compare temperature trends across decades. Historical data back to 1940 makes this trivial.

Pro Tips

  1. Use the geocoding API to find coordinates by city name:
curl "https://geocoding-api.open-meteo.com/v1/search?name=Tokyo"
  1. Combine with IP-API for auto-location:
import requests

# Get user location from IP
loc = requests.get("http://ip-api.com/json/").json()
# Get weather for that location
weather = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={loc['lat']}&longitude={loc['lon']}&current_weather=true").json()
print(f"Weather in {loc['city']}: {weather['current_weather']['temperature']}°C")
  1. Hourly data for charts:
curl "https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.01&hourly=temperature_2m,precipitation&forecast_days=3"

Need to collect weather data at scale for thousands of locations? I’ve built production-grade data extraction tools that handle batch API calls, scheduling, and data storage.

Need a custom weather data pipeline? Email me at [email protected]

What’s the most creative thing you’ve built with weather data? Share in the comments!

IP-API Has a Free API — Get Geolocation for Any IP Address Without an API Key

Every app that deals with users eventually needs to know where they are. Whether you’re personalizing content, blocking suspicious traffic, or logging analytics — IP geolocation is essential.

Most geolocation APIs require signup, API keys, and credit cards. IP-API gives you all of this for free — no key, no signup, no limits for non-commercial use.

What You Get

One HTTP request returns:

  • Country, region, city, zip code
  • Latitude and longitude
  • Timezone
  • ISP name and organization
  • AS number

Try It Right Now

curl http://ip-api.com/json/8.8.8.8

Response:

{
  "status": "success",
  "country": "United States",
  "regionName": "Virginia",
  "city": "Ashburn",
  "lat": 39.03,
  "lon": -77.5,
  "timezone": "America/New_York",
  "isp": "Google LLC",
  "org": "Google Public DNS",
  "query": "8.8.8.8"
}

That’s Google’s public DNS server — located in Ashburn, Virginia.

Python Example: Geolocate Any IP

import requests

def geolocate(ip):
    response = requests.get(f"http://ip-api.com/json/{ip}")
    data = response.json()
    if data["status"] == "success":
        return {
            "ip": ip,
            "location": f"{data['city']}, {data['regionName']}, {data['country']}",
            "coordinates": f"{data['lat']}, {data['lon']}",
            "isp": data["isp"],
            "timezone": data["timezone"]
        }
    return {"error": data.get("message", "Unknown error")}

# Try it
result = geolocate("1.1.1.1")
print(result)
# {'ip': '1.1.1.1', 'location': 'Los Angeles, California, United States', ...}

JavaScript (Node.js) Example

async function geolocate(ip) {
  const res = await fetch(`http://ip-api.com/json/${ip}`);
  const data = await res.json();

  if (data.status === "success") {
    console.log(`${ip} is in ${data.city}, ${data.country}`);
    console.log(`ISP: ${data.isp}`);
    console.log(`Coordinates: ${data.lat}, ${data.lon}`);
  }
}

geolocate("8.8.8.8");
// 8.8.8.8 is in Ashburn, United States
// ISP: Google LLC
// Coordinates: 39.03, -77.5

Batch Lookup (Up to 100 IPs)

Need to check multiple IPs at once? IP-API supports batch requests:

import requests

ips = ["8.8.8.8", "1.1.1.1", "208.67.222.222"]
response = requests.post(
    "http://ip-api.com/batch",
    json=ips
)

for result in response.json():
    print(f"{result['query']}: {result['city']}, {result['country']} ({result['isp']})")

# 8.8.8.8: Ashburn, United States (Google LLC)
# 1.1.1.1: Los Angeles, United States (Cloudflare, Inc.)
# 208.67.222.222: San Francisco, United States (Cisco OpenDNS, LLC)

Real-World Use Cases

1. Security monitoring — A startup I worked with used IP-API to flag login attempts from unexpected countries. If a user usually logs in from Germany but suddenly appears from Nigeria, that’s a red flag.

2. Content personalization — Show currency, language, or shipping options based on visitor location without asking them.

3. Analytics dashboards — Map your API traffic geographically. Know which regions use your service most.

4. Fraud detection — Cross-reference IP geolocation with billing address. Mismatches often indicate fraud.

Rate Limits & Pro Plan

Feature Free Pro ($12.99/mo)
Rate limit 45 req/min Unlimited
HTTPS No Yes
API key Not needed Provided
Commercial use No Yes
Batch requests 100 IPs 1000 IPs

For development, testing, and personal projects — the free tier is more than enough.

Compared to Alternatives

Service Free Tier Key Required HTTPS Free
IP-API 45 req/min No No
ipinfo.io 50K/month Yes Yes
ipstack 100/month Yes No
ipgeolocation.io 30K/month Yes Yes
ip-api.com Pro Unlimited Yes Yes

IP-API wins on simplicity: no signup, no key, just curl and go.

Quick Tips

  1. Use fields parameter to get only what you need:
curl "http://ip-api.com/json/8.8.8.8?fields=country,city,lat,lon"
  1. Check your own IP:
curl http://ip-api.com/json/
  1. Get CSV format for scripts:
curl http://ip-api.com/csv/8.8.8.8

Need to process thousands of IPs for security research or analytics? I’ve built production-grade data extraction tools that handle IP enrichment at scale — no rate limits, no bans.

Need a custom geolocation pipeline? Email me at [email protected]

Have you used IP-API in a project? What’s your go-to geolocation service? Drop a comment below!

Why I built a React component library specifically for AI products

Last year I was building an AI chat interface. I needed a component
that shows streaming text with a blinking cursor, one that displays

tool calls in progress, and another that tracks token usage in real time.

I opened npm. Nothing.

Every UI library I found — shadcn, MUI, Chakra — was built for

traditional apps. Forms, tables, dashboards. Great libraries.
Wrong problem.

So I built Aura UI.

## What’s different

Most AI products need the same 9 things that no library provides:

  • MessageBubble — chat layout with role-based styling
  • StreamingIndicator — animated states (thinking / streaming / done)
  • ToolCallCard — shows what the agent is doing in real time
  • ParameterSlider — temperature, top-p controls with live preview
  • CitationBlock — source references with hover expand
  • TokenCounter — live usage with limit warnings
  • CodeBlock — syntax highlight + copy, optimized for LLM output
  • PromptEditor — textarea with token count + template variables
  • ModelSelector — provider/model picker with context window info

Plus 10 base components (Button, Input, Modal, etc.) that match the

same design language.

## Tech

React + TypeScript + Tailwind CSS. Dark/light mode.

Zero runtime dependencies beyond React itself.

## Try it

Live demo: https://aura-ui-mocha.vercel.app

If you’re building an AI product and this saves you a week of work,

it’s $49: https://payhip.com/b/z9Hjr

Happy to answer questions below — especially if there’s a component

you need that’s not on the list.

I built a file transfer tool that can’t spy on you even if it wanted to

I got tired of explaining privacy policies to people.

Every time I needed to send a file to someone, I had to pick a service and implicitly trust it. Trust that it wasn’t reading my files. Trust that it wasn’t training a model on my documents. Trust that when it said “we don’t look at your stuff” it actually meant it.

I couldn’t verify any of that. Neither could you.
So I built phntm.sh. And I want to be honest about what it is, what it isn’t, and where it’s still rough.

The core idea

Zero-knowledge means the server genuinely cannot read your files. Not “won’t.” Cannot.

Here’s how it works. When you drop a file into phntm, your browser generates a 256-bit AES key. The file gets encrypted client-side with AES-256-GCM before a single byte leaves your machine. Only the ciphertext goes to the server. The decryption key gets embedded in the URL fragment, the part after the #.

Here’s the important bit. Browsers never include the fragment in HTTP requests. It’s in the spec. RFC 3986. When you share a phntm link, the recipient’s browser downloads the ciphertext and decrypts it locally using the key from the fragment. My server never sees the key. Ever.

I store noise. Without the key, the ciphertext is mathematically useless.

Why open source

Because “trust us” is not an architecture.

I open-sourced both the web app and the CLI so you can verify the claims yourself. Don’t take my word for it. Read the crypto layer. Check that the key never gets sent anywhere. That’s the only honest thing to do when you’re making security claims.

The rough edges, and I mean it

I’m being honest here because this is build in public, not a product launch.

The encryption buffers the whole file in memory. For large files that’s a problem. I know. It’s on the list.

The CLI flag parsing is basic. I rolled it myself instead of using a library, which was a good learning exercise but means it’s not as robust as it should be.

I had Vercel Analytics on the page. A commenter flagged it and they were right to. The RFC holds. browsers don’t send fragments in HTTP requests. But Vercel Analytics reads location.href client-side, which includes the hash, and POSTs it to their endpoint. That’s a problem when the hash is your decryption key.

Fixed with a beforeSend hook that strips the fragment before the event fires. Lesson learned: audit every third party script when you’re making security claims, including the ones you added without thinking.

What it’s good for right now

Sending a file to someone who doesn’t have any tools installed. They get a link, they click it, it decrypts in their browser, it downloads. No account. No app. No signup.

The file self-destructs when the timer expires. Nothing to breach after that.

What I learned building this

I built the CLI in Go as my first real Go project. Not a tutorial project. Something I’d actually use.

The thing that made Go’s I/O model click for me was wrapping io.Reader to build a progress bar. The HTTP client does io.Copy and the bar updates itself as bytes flow through. Small thing but it changed how I think about composability.

The whole CLI is stdlib-only. No external deps. That was a deliberate choice and also a good constraint for learning.

Where it lives
https://phntm.sh
https://github.com/aliirz/phntm.sh
https://github.com/aliirz/phntm-cli

Feedback welcome. Especially on the crypto layer.