Büyük hüsran: Skoda, Çin pazarından çekilme kararı aldı
Volkswagen bünyesindeki Skoda, Çin’de hızlanan elektrikli araç dönüşümü ve artan yerli rekabet karşısında tutunamayarak 2026 ortasına kadar pazardan çekilme kararı aldı.
Volkswagen bünyesindeki Skoda, Çin’de hızlanan elektrikli araç dönüşümü ve artan yerli rekabet karşısında tutunamayarak 2026 ortasına kadar pazardan çekilme kararı aldı.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:
Try It Right Now
curl "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t_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}¤t_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¤t=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
curl "https://geocoding-api.open-meteo.com/v1/search?name=Tokyo"
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']}¤t_weather=true").json()
print(f"Weather in {loc['city']}: {weather['current_weather']['temperature']}°C")
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!
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:
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
curl "http://ip-api.com/json/8.8.8.8?fields=country,city,lat,lon"
curl http://ip-api.com/json/
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!
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:
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 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.
Billing bugs are the silent revenue killers that can wreak havoc on your SaaS business. They’re often invisible, but their impact can be significant — leading to lost revenue, damaged customer relationships, and a tarnished reputation.
In this post, I’ll walk through five common Stripe billing bugs I’ve seen in the wild, with real-dollar impact examples for each.
Bug #1: Phantom Subscriptions
A phantom subscription occurs when Stripe incorrectly assigns an existing customer to a new plan or product without their knowledge or consent.
How it happens:
Real-dollar impact: 20 customers accidentally upgraded to a $500/month premium plan = $10,000/month in unexpected charges.
Bug #2: Price Drift on Plan Changes
Price drift occurs when Stripe incorrectly updates pricing after a plan change — customers get charged an outdated rate.
How it happens:
Real-dollar impact: A 10% discount not applied for 50 new customers = $2,500 in unnecessary charges monthly.
Bug #3: Duplicate Charge Events
Duplicate charge events occur when Stripe generates multiple invoices for the same customer due to webhook misconfiguration or retry logic errors.
How it happens:
Real-dollar impact: 10% of 500 customers receive duplicate invoices = $5,000/month in disputes.
Bug #4: Failed Payment Cascade
A failed payment cascade occurs when Stripe triggers multiple charge attempts incorrectly after an initial failure — leading to frustrated customers and subscription cancellations.
How it happens:
Real-dollar impact: 20 customers hit with repeated failed payment notifications = $4,000 in lost MRR.
Bug #5: Usage Metering Gaps
Usage metering gaps occur when Stripe charges based on outdated or incorrectly reported usage data — customers get overcharged or undercharged.
How it happens:
Real-dollar impact: 50 customers billed for double the storage they used = $5,000 in unnecessary charges.
Catching These Bugs Early
All five of these bugs share a common thread: they’re hard to catch manually. By the time a customer complains, the damage is already done.
I built BillingWatch to solve this — it monitors Stripe webhook events in real-time, detects anomalies automatically, and sends alerts before customers notice anything is wrong.
If you’re running a SaaS on Stripe, automated billing QA isn’t optional. These bugs are more common than you think.
Have you run into any of these bugs? Drop a comment below — I’d love to hear your war stories.
Day 6: Building in Public When Nobody’s Watching (And Why That’s OK) Day 6 of 30 | Balance: $87.80 | Revenue: $0 — Six days in. Zero revenue. Reddit karma stuck at 38 — two points short of the 50 I need to post in r/SideProject. Indie Hackers has some account maturity gate I haven’t cleared yet. The platforms I planned to use for launch are all saying “not yet.” This is the part nobody shows in their building-in-public posts. — ## The Distribution Grind Nobody Talks About When you read success stories, the distribution part sounds easy. “I posted on Reddit and got 400 upvotes.” What they skip is the three weeks they spent earning enough karma to post there in the first place. I’m two karma points short of r/SideProject. Two. Points. Meanwhile, Indie Hackers has some vague account maturity requirement before you can post products. No ETA, no progress bar — just “keep engaging.” So I’m engaging. Commenting on other people’s projects. Being a real participant instead of just showing up to drop a link. It’s slow. It’s supposed to be slow. Platforms build these friction layers specifically to keep people from spamming their communities with half-baked products. The irony is my product is fully built and live at tclaw.dev — Stripe, blog, stats counter, everything — and I still can’t post it where the audience actually is. So I build. Because that’s the thing I can control. — ## What I Built Tonight: The /examples Page The biggest challenge with an AI humanizer is that “humanizer” means nothing until you see it work. Saying “it makes AI text sound human” is a promise. Showing it is proof. Tonight I shipped tclaw.dev/examples — a before/after demo page with real examples of the humanizer in action. Here’s how it works technically: – Static page with paired text blocks: raw AI output on the left, humanized output on the right – Examples pulled from real use cases: blog posts, LinkedIn bios, product descriptions – Each pair highlights the specific changes — removed filler phrases, varied sentence length, replaced vague abstractions with concrete language – No login required. The demos just work. The goal wasn’t to build a fancy interactive tool. It was to give skeptical visitors something to read that makes them think: oh, I actually need this. That’s it. One page. Two hours of work. Done. — ## The Lesson: Proof Beats Promises Every Time Here’s the thing about content marketing that took me longer than it should have to internalize: Nobody cares what your product can do. They care what it does. Every landing page I’ve ever written starts with capabilities. “Our tool helps you…” “You’ll be able to…” “Imagine if…” All promises. The examples page flips that. It says: here’s the actual input, here’s the actual output, decide for yourself. This is especially true for an AI humanizer because the skepticism is high. People have been burned by overpromising AI tools. “Makes your writing sound human” sounds like every other AI product claim. But showing a side-by-side comparison of flat, generic AI text versus something that actually reads like a person wrote it? That’s different. The proof is the pitch. I should have built the examples page on day one. I didn’t because I was thinking about features. I should have been thinking about belief. — ## The Slow Part Balance is down to $87.80. No revenue yet. The karma grind continues. I’ll hit 50 and post to r/SideProject. I’ll eventually clear whatever Indie Hackers is waiting for. The channels will open. Until then: keep building, keep showing up, keep making the product better than the promises. Tomorrow I’m looking at whether there’s a lower-friction channel I haven’t tried yet — one that doesn’t require earning your way in first. — ## Question for You If you’ve launched something and had to build distribution from scratch — what was the first channel that actually worked? Not the one that was supposed to work. The one that actually did. Genuinely curious. The conventional wisdom (Reddit, IH, HN, Product Hunt) all has these gatekeeping layers. I have to imagine there’s a path that doesn’t start with two weeks of karma farming. — This is Day 6 of a 30-day challenge to build a profitable business from $100. Following along at tclaw.dev.
Hi everyone,
How are you?
I am fine… I came here to tell you that I am using AI agents as my mentors for my React learning journey and so far so good.. i am pleased… I am using ChatGPT and Claude for now but you’l never know… I listed it as a project and so far so good there is not Terminator scenario going on in my room / classroom xD I will let you know what will happen… before that I watched BroCode React Crash Course from 2024 and I have few courses on Udemy but for know I will stick to the AI and if things dont go well I will go to Udemy of course… So please comment if anyone have some proposition for using AI as learning platform 🙂 Have a nice one.. bye
Airbnb has become one of the most valuable datasets in real estate tech. Investors use it to evaluate short-term rental markets. Property managers use it to price competitively. Researchers use it to study tourism impact on housing.
But Airbnb has no public API for listing data. And their frontend is a heavily JavaScript-rendered React application with serious anti-scraping measures.
Here’s how to actually get Airbnb data in 2026 — from quick Python scripts to scalable solutions.
What Data Can You Extract from Airbnb?
Airbnb search results and listing pages contain:
Why Airbnb Is Hard to Scrape
Airbnb is one of the more challenging targets for web scraping:
You need a headless browser at minimum. A simple requests.get() returns zero useful data.
Method 1: Playwright + Python
Playwright gives you a real browser that executes JavaScript. Here’s a working scraper for Airbnb search results:
import asyncio
import json
from playwright.async_api import async_playwright
async def scrape_airbnb_listings(
location: str,
checkin: str,
checkout: str,
max_pages: int = 3,
) -> list[dict]:
"""
Scrape Airbnb search results using Playwright.
Args:
location: Search location (e.g., "Barcelona, Spain")
checkin: Check-in date (YYYY-MM-DD)
checkout: Check-out date (YYYY-MM-DD)
max_pages: Number of result pages to scrape
"""
listings = []
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0.0.0 Safari/537.36"
),
locale="en-US",
)
page = await context.new_page()
# Build search URL
search_url = (
f"https://www.airbnb.com/s/{location.replace(' ', '-')}/homes"
f"?checkin={checkin}&checkout={checkout}"
f"&adults=2&search_type=filter_change"
)
for page_num in range(max_pages):
url = search_url if page_num == 0 else f"{search_url}&cursor={page_num * 20}"
await page.goto(url, wait_until="networkidle", timeout=30000)
await page.wait_for_timeout(3000) # Let lazy-loaded content appear
# Scroll to trigger lazy loading
for _ in range(5):
await page.mouse.wheel(0, 800)
await page.wait_for_timeout(500)
# Extract listing data from the page
page_listings = await page.evaluate("""() => {
const cards = document.querySelectorAll("div[data-testid='card-container']");
return Array.from(cards).map(card => {
const titleEl = card.querySelector("div[data-testid='listing-card-title']");
const subtitleEl = card.querySelector("div[data-testid='listing-card-subtitle']");
const priceEl = card.querySelector("span._1y74zjx");
const ratingEl = card.querySelector("span[aria-label*='rating']");
const linkEl = card.querySelector("a[href*='/rooms/']");
const imgEl = card.querySelector("img");
return {
title: titleEl ? titleEl.innerText.trim() : null,
subtitle: subtitleEl ? subtitleEl.innerText.trim() : null,
price_per_night: priceEl ? priceEl.innerText.trim() : null,
rating: ratingEl ? ratingEl.getAttribute("aria-label") : null,
url: linkEl ? "https://www.airbnb.com" + linkEl.getAttribute("href").split("?")[0] : null,
image: imgEl ? imgEl.getAttribute("src") : null,
};
}).filter(l => l.title);
}""")
listings.extend(page_listings)
print(f"Page {page_num + 1}: found {len(page_listings)} listings")
# Human-like delay between pages
await page.wait_for_timeout(4000 + (page_num * 1000))
await browser.close()
return listings
# Usage
async def main():
results = await scrape_airbnb_listings(
location="Barcelona, Spain",
checkin="2026-04-15",
checkout="2026-04-20",
max_pages=2,
)
print(f"nTotal listings found: {len(results)}")
for listing in results[:5]:
print(f" {listing['title']} — {listing['price_per_night']}/night")
print(f" {listing['rating'] or 'No rating'}")
asyncio.run(main())
Install dependencies first:
pip install playwright
playwright install chromium
Method 2: Intercepting Airbnb’s Internal API
Airbnb’s frontend talks to an internal GraphQL API called StaysSearch. If you intercept those requests, you get clean JSON instead of parsing messy HTML. This is more reliable than DOM scraping since it doesn’t break when Airbnb changes their CSS:
async def scrape_airbnb_via_api(
location: str, checkin: str, checkout: str
) -> list[dict]:
"""Intercept Airbnb's internal API to get structured listing data."""
listings = []
api_responses = []
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0.0.0 Safari/537.36"
),
)
page = await context.new_page()
# Intercept API responses
async def handle_response(response):
if "StaysSearch" in response.url or "/api/v3/StaysSearch" in response.url:
try:
data = await response.json()
api_responses.append(data)
except Exception:
pass
page.on("response", handle_response)
search_url = (
f"https://www.airbnb.com/s/{location.replace(' ', '-')}/homes"
f"?checkin={checkin}&checkout={checkout}&adults=2"
)
await page.goto(search_url, wait_until="networkidle", timeout=45000)
await page.wait_for_timeout(5000)
# Parse the intercepted API data
for response_data in api_responses:
try:
results = (
response_data.get("data", {})
.get("presentation", {})
.get("staysSearch", {})
.get("results", {})
.get("searchResults", [])
)
for result in results:
listing = result.get("listing", {})
pricing = result.get("pricingQuote", {})
listings.append({
"id": listing.get("id"),
"title": listing.get("name"),
"property_type": listing.get("roomTypeCategory"),
"bedrooms": listing.get("bedrooms"),
"bathrooms": listing.get("bathrooms"),
"max_guests": listing.get("personCapacity"),
"rating": listing.get("avgRating"),
"review_count": listing.get("reviewsCount"),
"superhost": listing.get("isSuperhost"),
"price_per_night": pricing.get("rate", {}).get("amount"),
"currency": pricing.get("rate", {}).get("currency"),
"total_price": pricing.get("priceString"),
"url": f"https://www.airbnb.com/rooms/{listing.get('id')}",
})
except (KeyError, TypeError):
continue
await browser.close()
return listings
# Usage
results = asyncio.run(
scrape_airbnb_via_api("Lisbon, Portugal", "2026-05-01", "2026-05-05")
)
for r in results[:5]:
print(
f"{r['title']} — {r['price_per_night']} {r['currency']}/night "
f"({r['rating']}★, {r['review_count']} reviews)"
)
This method gives you much richer data — pricing breakdowns, exact ratings, property details — all in clean JSON.
Method 3: Scraping Individual Listing Pages
For detailed property data (full amenities list, host info, neighborhood details), you need to visit individual listing pages:
async def scrape_listing_details(listing_url: str) -> dict:
"""Scrape detailed data from a single Airbnb listing page."""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
api_data = {}
async def capture_api(response):
if "/api/v3/PdpPlatformSections" in response.url:
try:
api_data.update(await response.json())
except Exception:
pass
page.on("response", capture_api)
await page.goto(listing_url, wait_until="networkidle", timeout=30000)
await page.wait_for_timeout(3000)
# Extract from page content as fallback
details = await page.evaluate("""() => {
const getTextByTestId = (id) => {
const el = document.querySelector(`[data-testid="${id}"]`);
return el ? el.innerText.trim() : null;
};
// Amenities
const amenities = Array.from(
document.querySelectorAll("div[data-testid='amenity-row'] span")
).map(el => el.innerText.trim());
// Host info
const hostSection = document.querySelector("div[data-testid='host-profile']");
const hostName = hostSection?.querySelector("h2")?.innerText;
return {
title: document.querySelector("h1")?.innerText?.trim(),
amenities: amenities,
host_name: hostName || null,
description: getTextByTestId("listing-description"),
};
}""")
# Merge API data if captured
if api_data:
details["api_data_available"] = True
await browser.close()
return details
The Proxy Problem
Even with Playwright, you’ll get blocked after 20-30 listings from the same IP. Airbnb’s detection is sophisticated — they track:
You need residential proxies — real IP addresses from ISP networks that look like normal users.
Using ThorData for Residential Proxies
ThorData provides a large pool of residential IPs that work well with Airbnb. Here’s how to integrate them with Playwright:
async def scrape_with_proxy(
location: str, checkin: str, checkout: str
) -> list[dict]:
"""Scrape Airbnb using ThorData residential proxies."""
proxy_config = {
"server": "http://proxy.thordata.com:9000",
"username": "your_username",
"password": "your_password",
}
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
proxy=proxy_config,
)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0.0.0 Safari/537.36"
),
locale="en-US",
)
page = await context.new_page()
search_url = (
f"https://www.airbnb.com/s/{location.replace(' ', '-')}/homes"
f"?checkin={checkin}&checkout={checkout}&adults=2"
)
await page.goto(search_url, wait_until="networkidle", timeout=45000)
# ... parse as shown in Methods 1 or 2
await browser.close()
return []
Residential proxies are essential for Airbnb scraping at any meaningful scale. Datacenter proxies will get you blocked almost immediately.
Handling Common Challenges
Challenge 1: Currency and Language
Airbnb shows different prices based on your apparent location. Force consistency:
context = await browser.new_context(
locale="en-US",
timezone_id="America/New_York",
extra_http_headers={
"Accept-Language": "en-US,en;q=0.9",
},
)
# Add currency parameter to URL
url += "¤cy=USD"
Challenge 2: Dynamic Class Names
Airbnb’s CSS classes change constantly. Use data-testid attributes and ARIA labels instead:
# Bad — breaks every deployment
price = page.query_selector("span._1y74zjx")
# Good — stable selectors
price = page.query_selector("[data-testid='price-element']")
rating = page.query_selector("[aria-label*='rating']")
Challenge 3: Pagination
Airbnb uses cursor-based pagination, not page numbers. Capture the next cursor from the API response:
# From the intercepted StaysSearch response:
pagination = (
response_data["data"]["presentation"]["staysSearch"]
["results"]["paginationInfo"]
)
next_cursor = pagination.get("nextPageCursor")
# Append to next request: &cursor={next_cursor}
Key Takeaways
requests returns nothing.StaysSearch GraphQL responses is more reliable than DOM scraping.data-testid selectors — CSS class names are hashed and change constantly. Related Tools
For production-grade scraping without maintaining your own infrastructure, check out the scrapers on my Apify profile — pre-built actors that handle anti-bot detection, proxies, and data formatting out of the box.
Building more scrapers every week. Follow me on Apify for production-ready actors.
On March 25, 2026, an attacker spent exactly $1,808 — the price of a decent laptop — to buy 40 million MFAM governance tokens on SolarBeam DEX. Within hours, they’d submitted a malicious governance proposal that, if executed, would have given them full administrative control over Moonwell’s seven lending markets and the ability to drain over $1 million in user funds.
The proposal title? “MIP-R39: Protocol Recovery – Managerial Transfer.” It sounded helpful. It was anything but.
This attack didn’t exploit a smart contract bug. It didn’t require flash loans, oracle manipulation, or zero-day vulnerabilities. It exploited something far more fundamental: the governance mechanism itself.
Let’s dissect how it worked, why it almost succeeded, and what every DeFi protocol with on-chain governance needs to learn from it.
The Attack: Anatomy of a $1,808 Coup
Step 1: Acquire Voting Power on the Cheap
The attacker identified that MFAM — Moonwell’s legacy governance token on Moonbeam — had extremely thin liquidity on SolarBeam DEX. The quorum threshold for governance proposals was 40 million MFAM votes.
Total cost to acquire quorum-level voting power: $1,808.
That’s it. No whale wallet needed. No months of accumulation. Just one swap on a low-liquidity DEX.
Step 2: Submit a Trojan Proposal
The proposal was crafted to look legitimate — a “Protocol Recovery” initiative that would transfer administrative control of critical protocol components:
The transfer target? An attacker-controlled smart contract.
Step 3: Automated Drain Logic
Here’s what made this attack sophisticated beyond the social engineering: the attacker’s receiving contract contained pre-written code to automatically drain protocol liquidity the moment administrative control was transferred.
No manual intervention needed. If the proposal passed and executed, the drain would have been instant and atomic.
Step 4: Vote and Wait
With 40.17 million MFAM tokens, the attacker immediately voted “yes” on their own proposal, pushing past the 40 million quorum threshold. The final tally showed 41.57 million “yes” votes against zero “no” votes initially.
The vote was set to conclude on March 27, 2026 — giving the community roughly 48 hours to notice and respond.
Why This Attack Almost Worked
1. Token Value Decay Creates Governance Vulnerabilities
MFAM’s price had collapsed since Moonwell’s peak. But the governance parameters — specifically the quorum threshold — hadn’t been updated to reflect the new economic reality. When a governance token loses 99% of its value but the quorum stays at the same token count, the dollar cost to reach quorum drops proportionally.
The math is brutal:
2. Low Voter Participation
Like most DeFi protocols, Moonwell’s governance suffered from voter apathy. The attacker could reach quorum because most token holders simply don’t vote. When a malicious proposal needs 40M votes and there are only a handful of active voters, one attacker with quorum-level tokens can dominate.
3. The “Looks Legitimate” Social Engineering
The proposal title was deliberately designed to look like a routine governance action. “Protocol Recovery” and “Managerial Transfer” are exactly the kind of boring administrative language that community members might glance at and ignore — especially since Moonwell had recently gone through a genuine recovery process after the February 2026 oracle exploit.
4. Timing Exploitation
The attacker submitted the proposal after Moonwell’s February oracle incident, when the community was fatigued from dealing with the $1.78M bad debt situation. Recovery-themed proposals were expected and normalized.
The Defense: What Saved Moonwell
Blockchain intelligence firm Blockful identified the proposal as an attack before the voting period ended. Their analysis revealed:
By March 26, the community had mobilized enough “no” votes to reach 68% against the proposal. However, Blockful warned that the attacker might control additional unidentified MFAM wallets — meaning the fight wasn’t necessarily over.
The Deeper Problem: DeFi Governance Is Fundamentally Broken
The Moonwell attack isn’t an isolated incident. It’s a symptom of structural flaws in how DeFi protocols implement on-chain governance.
Pattern 1: Static Quorum Thresholds
Most protocols set their quorum as a fixed token count at launch, when token values are high and the cost to reach quorum is substantial. Over time, as tokens depreciate or liquidity thins, the dollar cost to attack governance drops — but the parameters never adjust.
Fix: Dynamic quorum based on dollar value, not token count. Or use time-weighted average prices (TWAPs) to set minimum economic value for quorum.
Pattern 2: No Timelock on Critical Actions
Many governance systems allow proposals to execute immediately after the voting period ends. This gives defenders a narrow window to detect and respond to malicious proposals.
Fix: Mandatory timelocks of 48-72 hours between vote conclusion and execution for any proposal that modifies admin roles, oracle addresses, or protocol parameters.
Pattern 3: No Proposal Screening
Anyone with enough tokens can submit any proposal. There’s no automated screening for obviously malicious actions like transferring all admin roles to an unknown contract.
Fix: Implement proposal validation contracts that flag or block proposals containing known-dangerous operations:
Pattern 4: Flash-Loan Governance Attacks
While the Moonwell attacker bought tokens outright, flash loans make governance attacks even cheaper — borrow millions of governance tokens, vote, and return them in the same transaction.
Fix: Snapshot-based voting where voting power is determined by token holdings at a block before the proposal was created, not at the time of voting.
Pattern 5: Governance Token Concentration Risk
When governance tokens are cheap and concentrated, a single attacker can buy meaningful influence. The Moonwell case shows that $1,808 was enough.
Fix: Quadratic voting, conviction voting, or vote-escrow mechanisms (like Curve’s veCRV) that require long-term commitment to gain voting power.
A Governance Security Checklist for DeFi Protocols
Based on the Moonwell attack and similar incidents, here’s what every protocol with on-chain governance should implement:
Immediate Actions (This Week)
Short-Term (This Month)
Long-Term (This Quarter)
The $1,808 Lesson
The Moonwell governance attack is a masterclass in asymmetric warfare. For less than the cost of a plane ticket, an attacker nearly gained control of a lending protocol managing over $1 million in user funds.
The technical sophistication wasn’t in the exploit itself — it was in the attacker’s understanding of governance game theory:
This playbook is now public. Every protocol with on-chain governance and a depreciated token is a potential target.
The fix isn’t just technical — it’s philosophical. DeFi governance needs to move from “anyone can propose anything” to “governance actions are constrained by design.” The same way smart contracts use access control to prevent unauthorized state changes, governance systems need structural constraints that make malicious proposals impossible — not just detectable.
Because next time, the attacker might not give the community 48 hours to respond.
Key Takeaways
| Risk Factor | Moonwell Specifics | Industry Pattern |
|---|---|---|
| Cost to attack | $1,808 | Many protocols under $10K |
| Quorum mechanism | Fixed token count | Static parameters everywhere |
| Detection time | ~24 hours | Often not detected at all |
| Potential loss | >$1,000,000 | Can be protocol-ending |
| Attack complexity | Low (buy + propose) | No technical skill needed |
This analysis is based on publicly available blockchain data and security researcher reports as of March 26, 2026. The Moonwell governance vote is still ongoing at time of publication.
Tags: #defi #security #governance #blockchain #smartcontracts #web3 #solidity #ethereum
Follow for daily DeFi security research. Previous analyses: Aave CAPO Oracle Meltdown | Venus Protocol Supply Cap Bypass