Install
openclaw skills install gasbuddy-browserUse 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 return top station prices. Trigger when users ask to check gas prices on GasBuddy, compare stations, or automate GasBuddy search/sort/filter steps.
openclaw skills install gasbuddy-browser⚠️ 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.
https://www.gasbuddy.com/gasprices/ontario/north-york)[class*="stationListItem"] selector and price regexconst { 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();
})();
The key insight is time-based waiting:
navigator.webdriver=true is not a blocking factor — Cloudflare passes Playwright browsers--disable-blink-features=AutomationControlled and custom UA are not required167.9¢ (means $1.679/L)/^\d{3}$/ for 3-digit prices was wrong/(\d+\.\d+)\s*¢/ to match 167.9¢scrollTo(0, document.body.scrollHeight) once is not enoughscrollBy(0, 600) multiple times with 400ms waitslines[0]) may be the main brand (e.g., "Esso") but sub-brands get lostGasBuddy home search is unreliable in automation. Always use direct city URL.
https://www.gasbuddy.com/gasprices/ontario/north-yorkhttps://www.gasbuddy.com/gasprices/ontario/torontohttps://www.gasbuddy.com/gasprices/ontario/markhamhttps://www.gasbuddy.com/gasprices/ontario/richmond-hillhttps://www.gasbuddy.com/gasprices/ontario/scarboroughhttps://www.gasbuddy.com/gasprices/ontario/vaughanPrice format: XXX.X¢ (one decimal, cents per liter, e.g. 167.9¢ = $1.679/L)
/(\d+\.\d+)\s*¢/ to extract — do NOT use integer regexCSS class for station cards: [class*="stationListItem"]
Lines within each card (after text split by newline):
/\d+\.\d+\s*¢/ anywhere in the text🔥 {City} Gas Prices (Regular 87) — {Date}
========================================
1. Brand — $X.XXX/L (XXX.X¢) — Address
2. Brand — $X.XXX/L (XXX.X¢) — Address
...
browser tool — it gets blocked every time by Cloudflare/^\d{3}$/) — prices have one decimal: 167.9¢