# GasBuddy Scraper: Fuel Prices, Stations & Reviews (`abotapi/gasbuddy-com-scraper`) Actor

Scrape GasBuddy station by station across the US and Canada. Returns brand, address, coordinates, phone, amenities, open status, ratings and reviews, plus the current cash and credit price for every fuel grade with who reported it and when. Incremental mode tracks price changes between runs.

- **URL**: https://apify.com/abotapi/gasbuddy-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 station results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## GasBuddy Station and Fuel Price Scraper

Station-by-station fuel prices from **GasBuddy**, across the United States and Canada.

Most fuel-price datasets stop at a national or state average. This one goes down to the pump: for
every station it returns the brand, the full postal address, coordinates, phone, amenities, opening
status, member rating and reviews, and the **current cash and credit price for every fuel grade the
station sells**, together with **when that price was reported and which member reported it**.

Turn on **Incremental mode** and the same search on a schedule becomes a price history: every run
returns only the stations whose prices moved, with `changedFields` naming exactly what changed.

### What you get

| | |
|---|---|
| Coverage | United States and Canada |
| Grades | Regular, Midgrade, Premium, Diesel, E85, UNL88 (E15) |
| Per grade | cash price, credit price, formatted price, member discount, reported-at timestamp, reporting member |
| Station | id, name, brand, brand logo, address, city, state, postal code, country, latitude, longitude, phone |
| Extras | amenities, opening status and hours, star rating, ratings count, reviews, top price spotters, member discount offers, card payment availability, outage flag |
| Area context | the city, state and country price trend GasBuddy publishes beside the station (today's average, today's low, direction) |
| Change tracking | `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt` |

### Modes

**Search by location.** List one or more areas under *Locations*. Each entry can be:

| Entry | What it does |
|---|---|
| `78701` | every station in that postal code |
| `Austin, TX` | every station the site indexes for that city |
| `Texas` | a state-wide sweep |
| `30.2672,-97.7431` | a radius search around a point, sorted by distance |

Only a **coordinate pair** makes GasBuddy return a per-station distance, so *Max distance (miles)*
only narrows those entries. A station with no distance is kept, never silently dropped.

**Paste links.** Switch *mode* to `url` and paste station addresses
(`https://www.gasbuddy.com/station/12345`), bare station ids (`12345`), or a search address
(`https://www.gasbuddy.com/home?search=78701`). A station you paste by hand is always returned, even
when the filters below would have excluded it.

**Splitting a big sweep across runs.** Append `&page=2` (or `&cursor=50` for an exact row offset) to a
pasted search address and the walk starts there instead of at the first result. GasBuddy's own
addresses never carry a position, so this is this actor's convention, not the site's. A run that
starts deeper is never treated as a complete scan, so Incremental mode can never mark the rows it was
told to skip as disappeared.

### Filters

| Input | What it narrows |
|---|---|
| Fuel brands | Shell, Costco, BP, ... checked against GasBuddy's own brand list, so a typo is reported by name instead of silently returning everything |
| Fuel grade | which grade GasBuddy ranks the area by; every grade the station sells is still returned |
| Maximum price age | only stations whose price was confirmed in the last 4 / 8 / 12 / 24 / 36 / 48 hours |
| Minimum / maximum price | on the station's cheapest reported price |
| Minimum star rating | member rating |
| Required amenities | keeps only stations that have **all** of the amenities you pick |
| Open right now only | current opening status |
| Only stations with a reported price | drops stations nobody has priced |
| Max distance (miles) | coordinate searches only |

### Every input, in one table

| Section | Input | Default |
|---|---|---|
| Mode | What do you want to scrape? | `search` |
| Search mode | Locations | (empty) |
| URL mode | Station or search links | (empty) |
| Filters | Fuel brands | (any) |
| Filters | Fuel grade | Regular |
| Filters | Maximum price age (hours) | Any age |
| Filters | Minimum price | (none) |
| Filters | Maximum price | (none) |
| Filters | Minimum star rating | (any) |
| Filters | Required amenities | (none) |
| Filters | Open right now only | off |
| Filters | Only stations with a reported price | off |
| Filters | Max distance (miles) | (none) |
| Output | Include member reviews | off |
| Output | Max reviews per station | 10 |
| Output | Max stations | 20 |
| Output | Max pages per location | 0 (walk to the end) |
| Resume | Resume from a previous run | (none) |
| Resume | Incremental mode (track price changes between runs) | off |
| Resume | State key | derived from the search |
| Resume | Also return unchanged stations | off |
| Resume | Also return stations that disappeared | off |
| Connection | Connection | Apify Proxy, country `US` (reaches Canada too) |

### Recurring price monitoring

Set **Incremental mode** on and schedule the run. The actor remembers the previous run of the same
search by itself, so no run id has to be pasted anywhere.

| Label | Meaning |
|---|---|
| `NEW` | first time this station appeared in this search |
| `UPDATED` | something changed; `changedFields` names what |
| `UNCHANGED` | nothing moved. Suppressed by default, so you are not billed for it |
| `REAPPEARED` | it was gone, and it is back |
| `EXPIRED` | it is no longer found. Only after a run that completed a full scan |

*Also return unchanged stations* and *Also return stations that disappeared* both default to off,
because both add extra rows and extra rows are billed like any other result.

**Fields that never appear in `changedFields`, and why.** `priceTrend` is an area aggregate shared by
every station in a metro and recomputed continuously, so tracking it would relabel a whole city as
changed with no station-level change behind it. `offers` carries a fresh opaque id on nearly every
fetch for the same promotion. `reviews` and `topSpotters` are excluded on size, because a busy
station's review corpus would not fit in the saved baseline. Nothing is lost: `reviewCount`,
`ratingsCount` and `starRating` are all tracked, so a new review still shows up as an `UPDATED`
station naming those fields. `distance`, `searchTerm`, `searchLocation` and `scrapedAt` describe the
search you ran, not the station.

### Resuming an interrupted crawl

*Resume from a previous run* takes one run id (or dataset id) and skips the stations that run already
returned. Use it to finish one interrupted crawl. For recurring monitoring use Incremental mode
instead, which needs no id at all.

### Sample output (abridged)

```json
{
  "stationId": "12345",
  "name": "Example Fuel",
  "url": "https://www.gasbuddy.com/station/12345",
  "brand": "Example Fuel",
  "brandId": "99",
  "addressLine1": "123 Main St",
  "city": "Example City",
  "state": "TX",
  "postalCode": "78701",
  "country": "US",
  "latitude": 30.27,
  "longitude": -97.74,
  "openStatus": "open",
  "amenities": ["C-Store", "Pay At Pump", "Air Pump"],
  "fuels": ["regular_gas", "midgrade_gas", "premium_gas", "diesel"],
  "prices": [
    {
      "fuelType": "regular",
      "label": "Regular",
      "cashPrice": null,
      "creditPrice": 3.47,
      "creditPostedAt": "2026-08-27T23:35:40.393Z",
      "creditReportedBy": "member-handle",
      "formattedPrice": "$3.47",
      "memberDiscount": 0
    },
    {
      "fuelType": "midgrade",
      "label": "Midgrade",
      "cashPrice": null,
      "creditPrice": null,
      "creditPostedAt": null,
      "creditReportedBy": null,
      "formattedPrice": null,
      "memberDiscount": 0
    }
  ],
  "lowestPrice": 3.47,
  "lowestPriceFuelType": "regular",
  "lastPriceReportedAt": "2026-08-27T23:35:40.393Z",
  "lastPriceReportedBy": "member-handle",
  "priceUnit": "dollars_per_gallon",
  "currency": "USD",
  "starRating": 3.4,
  "ratingsCount": 80,
  "reviewCount": 42,
  "priceTrend": [
    { "areaName": "Example City", "todayAverage": 3.5, "todayLow": 3.22, "direction": "falling" }
  ],
  "changeType": "UPDATED",
  "changedFields": ["lowestPrice", "prices"]
}
```

A grade nobody has priced comes back as `null`, never as `0`. GasBuddy itself renders it as
`- - -`, and a zero would poison any average you compute over the dataset.

The reporting member's public handle is kept because a crowd-sourced price feed is exactly what
GasBuddy is, and the handle is what links a review to the price reports beside it. Member avatar
images are **not** collected. The sample above uses placeholder handles rather than real ones.

### What you are charged for

| Event | When |
|---|---|
| Actor start | once per run, per GB of memory |
| Station result | each station returned in the dataset |
| Extra review pages | once per station that needed an **extra** review request |

Reviews are cheap by design: the first page of reviews for every station arrives inside the search
request itself and is never charged. Only raising *Max reviews per station* above what that page
already carries buys extra requests, and only those stations are charged.

### Limits and honest caveats

- **Price history is per run, not per second.** GasBuddy's public data publishes the *current*
  reported price per grade plus the area's daily average, low and direction. It does not publish a
  per-station time series. Incremental mode is how you build one: schedule the run, and every price
  move arrives as an `UPDATED` row with `changedFields`.
- **The site answers North America only.** Keep the connection country set to `US`. A run from
  anywhere else is refused before it reaches any data, and the run fails with a message that says so
  rather than reporting an empty search. A US exit reaches the **Canadian** stations too: a Toronto
  search returns Canadian rows with `currency: CAD` and `priceUnit: cents_per_liter`, so no Canadian
  exit is needed.
- **`brand` is null for an unbranded independent station.** GasBuddy indexes them with an empty brand
  list, and a wide sweep will reach them. `name` is always populated.
- `hours` is not published for every station. `openStatus` is, and is populated far more often.
- A postal-code search returns only the stations the site indexes inside that code, which is often a
  handful. Use a coordinate pair, a city, or a state for wider coverage.

### Local run

```bash
python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m pytest tests -q
apify run --purge
```

### Verification

2026-09-01: audit re-derived the live behaviour through the actor's own session harness. For an
Austin, TX search the site reports 403 stations; the fuel-grade filter narrows the server count to
283 (diesel), the brand filter to 64 (Shell) and 1 (Costco), and the price-age window narrows the
walked page from 50 to 25 rows. The local filters narrow monotone on returned rows: a price ceiling
keeps 1 then 10 of 50, a floor keeps 49, a 4-star floor keeps 25, and a required amenity keeps 39,
each row satisfying the filter. Both URL shapes verified in a cloud run: a station link returns that
station and a search link starts the walk at its results.

# Actor input Schema

## `mode` (type: `string`):

Search mode looks up stations around each location you list. URL mode reads the station links you paste. Fill in the section that matches your choice, then set the filters below.

## `locations` (type: `array`):

One entry per area. Use a postal code (78701), a city and state (Austin, TX), a state (Texas), or a coordinate pair written as latitude,longitude (30.2672,-97.7431). A coordinate pair is what makes this a radius search: only then does GasBuddy return a distance per station, which is what Max distance filters on. Search mode only.

## `urls` (type: `array`):

Paste station addresses such as https://www.gasbuddy.com/station/29330, bare station ids such as 29330, or a search address such as https://www.gasbuddy.com/home?search=78701. Append \&page=2 (or \&cursor=50 for an exact row offset) to a search address to start the walk deeper, which is how a big sweep is split across runs. A pasted station is always returned, even when the filters below would have excluded it. URL mode only.

## `brands` (type: `array`):

Return only stations of these brands, for example Shell or Costco. Names are checked against GasBuddy's own brand list, so a misspelled brand is reported by name instead of silently returning everything. Several brands return stations of ANY of them. Applies to a location search and to a pasted search link.

## `fuelType` (type: `string`):

The grade GasBuddy ranks the area by. Every grade the station sells is still returned in the prices list; this decides which grade drives the ordering and which stations qualify.

## `maxPriceAgeHours` (type: `string`):

Prices on GasBuddy are reported by its members. Set a window to keep only stations whose price was confirmed recently.

## `minPrice` (type: `integer`):

Keep stations whose cheapest reported price is at least this much. Left empty means no lower bound.

## `maxPrice` (type: `integer`):

Keep stations whose cheapest reported price is at most this much. Left empty means no upper bound.

## `minStarRating` (type: `integer`):

Keep only stations rated at least this many stars by GasBuddy members. Left empty means any rating, including unrated stations.

## `amenities` (type: `array`):

Keep only stations that have ALL of the amenities you pick. GasBuddy publishes no amenity filter of its own, so this is applied to the stations it returns.

## `openNowOnly` (type: `boolean`):

Keep only stations GasBuddy currently reports as open.

## `withPricesOnly` (type: `boolean`):

Keep only stations that have at least one grade with a real reported price. Turn this on when an unpriced station is no use to you.

## `maxDistanceMiles` (type: `integer`):

Keep only stations within this many miles of the point you searched. Only a coordinate entry (latitude,longitude) makes GasBuddy return a distance, so a station with no distance is kept rather than dropped.

## `fetchReviews` (type: `boolean`):

Add GasBuddy member reviews to each station. The first page of reviews comes back with the search itself and is never charged. Only asking for MORE rows than that page holds costs an extra request per station, and only that is charged.

## `maxReviewsPerStation` (type: `integer`):

Upper bound on review rows per station. Up to 10 arrive free with the search; anything above that fetches extra pages and charges the review surcharge once for that station. 0 means no reviews.

## `maxStations` (type: `integer`):

Stop after this many matching stations across all locations. 0 means no limit, which on a whole state can be thousands of billed rows.

## `maxPages` (type: `integer`):

Stop each location after this many result pages. 0 means walk to the end of that area.

## `resumeFromRunId` (type: `string`):

Continue ONE interrupted crawl: paste that run's id (or its dataset id) and stations it already returned are skipped. For recurring monitoring use Incremental mode below instead, which remembers the previous run by itself.

## `incrementalMode` (type: `boolean`):

Run the same search on a schedule and get only what moved. Every station is labelled NEW, UPDATED, REAPPEARED or UNCHANGED, and changedFields names exactly what changed, which is how you build a price history over time. Unlike Resume from a previous run, this remembers the previous run itself.

## `stateKey` (type: `string`):

Name this monitoring campaign so several schedules can track different searches without sharing a baseline. Left empty, a key is derived from the mode, locations, links and every filter.

## `emitUnchanged` (type: `boolean`):

Incremental mode only. Returns stations whose prices did not move, labelled UNCHANGED. These are extra rows and they are billed like any other result.

## `emitExpired` (type: `boolean`):

Incremental mode only. Returns a final row labelled EXPIRED for a station that is no longer found, but only after a run that completed a full scan. These are extra rows and they are billed like any other result.

## `proxy` (type: `object`):

GasBuddy only answers from a North American exit, so keep the country set to US. A US exit reaches the Canadian stations too, with prices in CAD per litre, so there is no need to pick a Canadian exit (and the standard pool does not offer one on every plan). The default pool is enough; a residential pool is only ever used if you pick it here.

## Actor input object example

```json
{
  "mode": "search",
  "locations": [
    "78701"
  ],
  "fuelType": "regular",
  "maxPriceAgeHours": "0",
  "openNowOnly": false,
  "withPricesOnly": false,
  "fetchReviews": false,
  "maxReviewsPerStation": 10,
  "maxStations": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "locations": [
        "78701"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/gasbuddy-com-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "locations": ["78701"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/gasbuddy-com-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "locations": [
    "78701"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyCountry": "US"
  }
}' |
apify call abotapi/gasbuddy-com-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/gasbuddy-com-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TOFKZ4t5zckw3ObPn/builds/Ly8ZMnJC8OooJUSz0/openapi.json
