Write, debug, and tune Playwright specs with locator strategy, trace diagnosis, and CI-aware timeouts.
Browser
gasbuddy-browser
Try itUse Playwright via Bash `exec` tool (not the built-in browser tool) to fetch GasBuddy fuel prices for a target city/area (especially Toronto/North York), and...
What it does
**⚠️ Important: Use Playwright via tool, NOT the built-in tool.** The built-in OpenClaw Edge CDP browser cannot reliably wait out Cloudflare's time-based challenge. Direct Playwright via Node.js can navigate to the city URL and wait for the challenge to auto-resolve.
The skill document
GasBuddy Browser Skill
⚠️ Important: Use Playwright via
exectool, NOT the built-inbrowsertool. The built-in OpenClaw Edge CDP browser cannot reliably wait out Cloudflare's time-based challenge. Direct Playwright via Node.js can navigate to the city URL and wait for the challenge to auto-resolve.
Quick Workflow
- Navigate directly to the city gas price URL (North York as default, e.g.
https://www.gasbuddy.com/gasprices/ontario/north-york) - Wait for Cloudflare challenge to clear — title will change from "Just a moment..." to "Best Gas Prices & Local Gas Stations in..."
- Scroll incrementally to load all station cards
- Extract station data using
[class*="stationListItem"]selector and price regex - Return top 5 cheapest stations
Playwright Script Template
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({
headless: false,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu']
// No need for --disable-blink-features=AutomationControlled or custom UA
// Cloudflare challenge is time-based, not detection-based
});
const page = await browser.newPage();
await page.goto('https://www.gasbuddy.com/gasprices/ontario/north-york', {
timeout: 60000,
waitUntil: 'networkidle'
});
// Wait for Cloudflare to clear — check title changes away from challenge page
let attempts = 0;
while (attempts < 20) {
const title = await page.title();
if (!title.includes('Just a moment') && !title.includes('Cloudflare') && title.includes('Gas Prices')) break;
await page.waitForTimeout(3000);
attempts++;
}
// Scroll incrementally to trigger lazy loading of all station cards
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
for (let i = 0; i < 8; i++) {
await page.evaluate(() => window.scrollBy(0, 600));
await page.waitForTimeout(400);
}
await page.waitForTimeout(2000);
// Extract station data using the actual CSS class and price format
// If prices show "- - -" (unavailable), we retry with a page refresh
let stations = await page.evaluate(() => {
const cards = document.querySelectorAll('[class*="stationListItem"]');
return Array.from(cards).map(card => {
const text = card.innerText;
const lines = text.split('\n').map(l => l.trim()).filter(l => l);
// Price format is "XXX.X¢" (one decimal, e.g. "167.9¢")
const priceMatch = text.match(/(\d+\.\d+)\s*¢/);
const price = priceMatch ? priceMatch[1] : null; // e.g. "167.9"
// Check for "- - -" placeholder indicating unavailable price
const hasDashPrice = text.includes('- - -') || text.includes('— — —');
// Brand is first non-empty line
const brand = lines[0] || '';
// Address is a line starting with digit followed by letters
const address = lines.find(l => /^\d+\s+[A-Za-z]/.test(l)) || '';
return { brand, price, address, hasDashPrice };
}).filter(s => s.price && s.address);
});
// If prices show "- - -" (all have hasDashPrice or price is null), refresh and retry
const hasValidPrices = stations.length > 0 && stations.some(s => s.price && !s.hasDashPrice);
if (!hasValidPrices) {
console.log('Prices not loaded (showing "- - -"), refreshing...');
await page.reload({ timeout: 30000, waitUntil: 'networkidle' });
await page.waitForTimeout(3000);
// Re-scroll after reload
for (let i = 0; i < 5; i++) {
await page.evaluate(() => window.scrollBy(0, 600));
await page.waitForTimeout(400);
}
stations = await page.evaluate(() => {
const cards = document.querySelectorAll('[class*="stationListItem"]');
return Array.from(cards).map(card => {
const text = card.innerText;
const lines = text.split('\n').map(l => l.trim()).filter(l => l);
const priceMatch = text.match(/(\d+\.\d+)\s*¢/);
const price = priceMatch ? priceMatch[1] : null;
const brand = lines[0] || '';
const address = lines.find(l => /^\d+\s+[A-Za-z]/.test(l)) || '';
return { brand, price, address };
}).filter(s => s.price && s.address);
});
}
// Sort by price ascending
stations.sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
const date = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: 'short', day: 'numeric' });
console.log(`\n🔥 North York Gas Prices (Regular 87) — ${date}`);
console.log('='.repeat(55));
stations.slice(0, 8).forEach((s, i) => {
const p = parseFloat(s.price);
console.log(`${i+1}. ${s.brand} — $${(p/100).toFixed(3)}/L (${p}¢) — ${s.address}`);
});
await browser.close();
})();
Cloudflare Bypass Strategy
The key insight is time-based waiting:
- Cloudflare's managed challenge is time-based — it auto-resolves after a few seconds for any IP
navigator.webdriver=trueis not a blocking factor — Cloudflare passes Playwright browsers--disable-blink-features=AutomationControlledand custom UA are not required- Wait for title to include "Gas Prices" — that signals challenge is cleared
- Typical total time: 10-20 seconds
- Do NOT try to click through the challenge — just wait
Critical Corrections from Experience
Price Format is "XXX.X¢" (one decimal), NOT "XXX"
- Actual page format:
167.9¢(means $1.679/L) - The old regex
/^\d{3}$/for 3-digit prices was wrong - Correct approach: use
/(\d+\.\d+)\s*¢/to match167.9¢
Scrolling is Required to Load All Cards
- GasBuddy uses lazy loading — station cards only render as you scroll
scrollTo(0, document.body.scrollHeight)once is not enough- Must scroll incrementally:
scrollBy(0, 600)multiple times with 400ms waits - Without scrolling, only 1-3 stations appear; after scrolling 8+ times, 10+ stations appear
Brand Extraction Can Be Noisy
- Brand line (
lines[0]) may be the main brand (e.g., "Esso") but sub-brands get lost - The station name on the page is often "Esso & Circle K" — the "Circle K" part may not appear
- Brand extraction is good enough for the primary brand; don't over-engineer it
Practical Fallbacks
GasBuddy home search is unreliable in automation. Always use direct city URL.
Common Canada URL patterns
- North York:
https://www.gasbuddy.com/gasprices/ontario/north-york - Toronto:
https://www.gasbuddy.com/gasprices/ontario/toronto - Markham:
https://www.gasbuddy.com/gasprices/ontario/markham - Richmond Hill:
https://www.gasbuddy.com/gasprices/ontario/richmond-hill - Scarborough:
https://www.gasbuddy.com/gasprices/ontario/scarborough - Vaughan:
https://www.gasbuddy.com/gasprices/ontario/vaughan
Data Extraction Details
Price format: XXX.X¢ (one decimal, cents per liter, e.g. 167.9¢ = $1.679/L)
- Use regex
/(\d+\.\d+)\s*¢/to extract — do NOT use integer regex
CSS class for station cards: [class*="stationListItem"]
Lines within each card (after text split by newline):
- Line 0: Brand name (e.g., "Esso")
- Price: matched via regex
/\d+\.\d+\s*¢/anywhere in the text - Address: line starting with digit followed by letters (e.g., "6000 Dufferin St")
Filtering and Sorting
- Default is price ascending (cheapest first) — verify first 3 prices are non-decreasing
- Default fuel type shown: Regular (87)
- Open now / payment / brand filters require UI interaction — state as unavailable if requested
Output Template
🔥 {City} Gas Prices (Regular 87) — {Date}
========================================
1. Brand — $X.XXX/L (XXX.X¢) — Address
2. Brand — $X.XXX/L (XXX.X¢) — Address
...
Known Pitfalls
- Don't use the OpenClaw built-in
browsertool — it gets blocked every time by Cloudflare - Don't use integer regex for price (
/^\d{3}$/) — prices have one decimal:167.9¢ - Don't skip incremental scrolling — lazy loading means you miss most stations without it
- Cookie banners are automatically handled; no need to dismiss manually
- Location search on homepage is unreliable — always use direct city URL
- Price units are cents/liter (CAD), not dollars/gallon
Success Criteria
- Page title changes from "Just a moment..." to "Best Gas Prices & Local Gas Stations in..."
- At least 8 station cards extracted with brand, price, and address after scrolling
- Prices formatted as CAD/L (e.g., $1.679/L)
- Script completes within 60 seconds timeout
Related skills
Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.
Fetch raw ad creative, app, ranking, and revenue data from AdMapix as structured JSON.
Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.
Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.
Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.
More from mebusw
Browse all skillsManage scheduled Zoom meetings, cloud recordings, and account users from agent workflows via Server-to-Server OAuth.
Use when user asks to "generate workshop outline", "create training agenda", "design course structure", "build workshop schedule", or requests help planning...
Generate engaging short video scripts (30s-3min) for social media platforms like WeChat Channels, Douyin, Xiaohongshu, TikTok, and Reels. Use when user asks...
Use when user asks to "write WeChat article", "create 公众号文章", "write media content", or needs engaging articles for WeChat Official Accounts. Applies Pyramid...
Audit and improve existing Agent Skills (SKILL.md files) against the agentskills.io standard. Use this skill whenever the user wants to optimize, polish, imp...
Use this skill when publishing or managing posts on Xiaohongshu (小红书 / RED / xiaohongshu) via the official Creator Center. Triggers on requests to draft, save, or publish a note; generate titles, captions, or topic chips; reply to comments or DMs; or check creator dashboard metrics. The skill follow