# GasBuddy Fuel Prices — Drop-in, No Proxy Needed (`diopside/gasbuddy-prices`) Actor

Drop-in compatible with memo23/gasbuddy-fuel-prices-scraper — half the cost, no CAPTCHA fee. US & Canada gas-station prices from GasBuddy: every grade incl. E85 and UNL88, cash and credit, posted time, brands, ratings, offers, phone, coordinates, area price trends. Batch many ZIPs per run. No proxy.

- **URL**: https://apify.com/diopside/gasbuddy-prices.md
- **Developed by:** [DIOPSIDE AI](https://apify.com/diopside) (community)
- **Categories:** E-commerce, Travel
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 station with prices

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## GasBuddy Fuel Prices Scraper ⛽

**Drop-in compatible with `memo23/gasbuddy-fuel-prices-scraper` — less than half the cost per run, and no CAPTCHA fee.**
Gas-station fuel prices from GasBuddy across the **US and Canada**: every grade a station reports
(Regular, Midgrade, Premium, Diesel, **E85 and UNL88**), cash and credit, the time each price was
posted, plus brands, ratings, offers, Pay-with-GasBuddy availability, outage status, phone,
coordinates, and the city / state / national price trend the station sits in.

Batch dozens of ZIP codes or cities in one run. No residential proxy. No headless browser. No
CAPTCHA solver.

***

### Why this one

The leading GasBuddy actor charges **$0.07 per run start** to cover, in its own words, "a managed
CAPTCHA solve + residential proxy", on top of $0.0015 per station — and still fails about **16% of
its public runs**.

None of that is necessary. GasBuddy gates on the **TLS fingerprint**, not the exit IP: a Chrome
fingerprint gets a clean `200` from a plain datacenter IP where `curl` and `requests` get
Cloudflare's "Just a moment…". The `429` that follows about 15 requests is counted **per
connection** — not per IP and not per cookie — so a free reconnect resets it. This actor was built
from those two measurements, and prices accordingly.

| | **This actor** | `memo23/gasbuddy-fuel-prices-scraper` |
|---|---|---|
| Run start | **$0.00005** | $0.07 |
| Per station | **$0.001** | $0.0015 |
| **100-station run** | **$0.10** | **$0.22** |
| Proxy | **none needed** | residential, mandatory |
| Grades | **6** — incl. E85, UNL88 | 4 |
| Price freshness filter | **yes**, on the posted time actually read | no |
| Brand filter | **yes**, resolved from GasBuddy's live brand table | no |
| Canada | **yes** (CAD, cents/litre) | US only |
| Station phone / coordinates | **yes** | no |
| Area price trends | **yes** (city, state, national) | no |
| Unreported grade | **`null`** | — |

#### Switching from `memo23/gasbuddy-fuel-prices-scraper`

Change the actor id. Nothing else. The `search`, `fuel`, `lat`, `lng`, `maxStationsPerSearch`,
`maxPages`, `maxItems` and `maxRetries` inputs have the same names and meanings, `proxy` is still
accepted as an alias for `proxyConfiguration`, and every output field that actor emits is emitted
here under the same name and in the same shape. The new fields are added alongside; nothing is
renamed.

You can delete the `proxy` block while you are there — this actor does not need one.

***

### 📥 Input

| Field | Type | Description |
|---|---|---|
| `search` | array | Locations — US ZIP codes, city names, `"city, ST"`, Canadian cities. One run covers every entry. |
| `fuel` | select | Grade that drives GasBuddy's ranking — `Regular` (default), `Midgrade`, `Premium`, `Diesel`, `E85`, `UNL88`. **Every grade is still returned per station** regardless. |
| `brand` | string | *New.* Only stations of one brand, e.g. `"Costco"`, `"Shell"`. Matched against GasBuddy's own 663-brand table, read at run time. |
| `maxPriceAgeHours` | integer | *New.* Drop stations whose newest price is older than this. Skipped stations are not charged for. |
| `includeStationsWithoutPrices` | boolean | *New.* Keep stations nobody has posted a price for. Off by default; those rows are never charged for when dropped. |
| `lat` / `lng` | number | Optional coordinate search (provide both). Also fills `distance` on every row. |
| `maxStationsPerSearch` | integer | Cap per search location. Default `100`. |
| `maxPages` | integer | Cap on cursor pages per location. Default `10` (≈20–25 stations per page). |
| `maxItems` | integer | Hard cap on total rows. Default `1000`. |
| `maxRetries` | integer | Retries per request, each on a fresh connection. Default `5`. |
| `maxRequestsPerMinute` / `maxConcurrency` | integer | Politeness controls. Default `60` / `2`. |
| `proxyConfiguration` | proxy | Optional and off by default — see above. Also accepted as `proxy`. |

```json
{
  "search": ["Austin, TX", "90210", "Toronto, ON"],
  "fuel": "Regular",
  "maxStationsPerSearch": 50,
  "maxPages": 3,
  "maxItems": 500
}
```

#### A note on ZIP searches

A ZIP search returns only stations **inside that ZIP boundary**. Dense urban ZIPs legitimately
have none — `10001` (Manhattan) really does contain zero gas stations, on gasbuddy.com as much as
here. Use `"New York, NY"` or a `lat`/`lng` pair for a radius search.

***

### 📤 Output

One flat row per station; prices nested under `prices`, keyed by grade, and mirrored into flat
`regularPrice` / `midgradePrice` / … columns so the CSV export needs no post-processing.

```json
{
  "searchTerm": "Austin, TX",
  "primaryFuel": "Regular",
  "stationId": "81445",
  "name": "Valero",
  "brand": "Valero",
  "brands": [
    { "brandId": 142, "name": "Valero", "imageUrl": "https://images.gasbuddy.io/b/142.png", "brandingType": "fuel" },
    { "brandId": 32, "name": "Circle K", "imageUrl": "https://images.gasbuddy.io/b/32.png", "brandingType": "cstore" }
  ],
  "address": {
    "line1": "1706 E William Cannon Dr", "line2": null, "city": "Austin",
    "state": "TX", "postalCode": "78744", "country": "US",
    "full": "1706 E William Cannon Dr, Austin, TX, 78744"
  },
  "distance": null,
  "starRating": 3.8,
  "ratingsCount": 43,
  "payAvailable": true,
  "offers": [
    { "id": "H581445-2-0", "types": ["gasbuddy"], "use": ["strike", "sort"],
      "highlight": "Flash Deal: Save 2¢ per gallon",
      "discounts": [{ "grades": ["regular_gas", "diesel"], "pwgbDiscount": 0.02, "receiptDiscount": null }] }
  ],
  "emergencyStatus": { "hasGas": null, "hasDiesel": null, "hasPower": null },
  "hasActiveOutage": false,
  "enterprise": false,
  "fuels": ["regular_gas", "diesel"],
  "priceUnit": "dollars_per_gallon",
  "prices": {
    "Regular": {
      "fuelProduct": 1, "grade": "Regular", "cash": null,
      "credit": { "price": 3.49, "formattedPrice": "$3.49", "postedTime": "2026-09-22T03:24:00.328Z", "member": "DataFeed" },
      "discount": 0
    }
  },
  "searchCentroid": { "displayName": "Austin, TX", "latitude": 30.2989, "longitude": -97.7643, "regionCode": "TX", "countryCode": "US" },
  "stationUrl": "https://www.gasbuddy.com/station/81445",

  "phone": "512-444-0405",
  "latitude": 30.18937248975,
  "longitude": -97.767173051834,
  "countryCode": "US",
  "currency": "USD",
  "regularPrice": 3.49,
  "midgradePrice": null,
  "premiumPrice": null,
  "dieselPrice": null,
  "e85Price": null,
  "unl88Price": null,
  "pricesUpdatedAt": "2026-09-22T03:24:00.328Z",
  "priceAgeHours": 1.87,
  "areaTrends": {
    "city":    { "areaName": "Austin", "today": 3.89, "todayLow": 3.49, "trend": "down" },
    "region":  { "areaName": "Texas", "today": 3.96, "todayLow": 3.25, "trend": "up" },
    "country": { "areaName": "United States", "today": 4.46, "todayLow": 0, "trend": "up" }
  },
  "scrapedAt": "2026-09-22T05:16:19Z"
}
```

Export as **JSON, CSV or Excel** from the run's dataset.

#### `null` is not `$0.00`

GasBuddy returns an entry for every grade a station *sells*, and fills the ones nobody has
reported with `price: 0` and `formattedPrice: "- - -"`. Those are absences, not observations. This
actor emits them as `null` and leaves the grade out of `prices` — passed through verbatim they
read as diesel at $0.00 and quietly drag down any market average a buyer computes.

***

### ⚙️ How it works

GasBuddy serves its stations and prices from one GraphQL operation, `LocationBySearchTerm`, behind
three gates. This actor clears all three with plain HTTP:

1. **Cloudflare TLS fingerprinting.** `curl` and `httpx` get `403 cf-mitigated: challenge` on the
   first request from any IP. `curl_cffi` with a Chrome fingerprint gets `200` from the same IP in
   the same second — no challenge, no `cf_clearance`, no CAPTCHA.
2. **An Apollo CSRF preflight.** Without the `apollo-require-preflight` header, `/graphql` answers
   `400 Bad Request` as 11 bytes of plain text — which looks like a block and is not one. The
   `gbcsrf` token the site's own page carries is minted per session and sent alongside it.
3. **A per-connection rate limit.** After ~15 calls the endpoint answers `429`. Re-minting the
   token does not clear it; replaying the cookie in a new session does not trigger it; a new
   connection from the *same IP in the same second* is clean. So the counter is on the connection.
   The actor retires each connection at 12 calls and rotates instantly on a `429` — which costs a
   TCP handshake, not a residential IP.

Pagination follows GasBuddy's own `stations.cursor.next`, stations are de-duplicated by id across
overlapping searches so a shared station is never charged for twice, and a location that fails
does not take the rest of the run with it — what was collected is kept and the failure is named in
the run's status message.

***

### 💵 Pricing

Pay per event: **$0.00005 per run start** and **$0.001 per station row**. You pay for stations you
actually receive — stations skipped as priceless, stale, or as duplicates of one already returned
are never charged for. No monthly rental, no per-grade multiplication: one run returns every grade.

A 100-station run is **$0.10**.

***

### ❓ FAQ

**Do I need a proxy?** No. Leave `proxyConfiguration` off. GasBuddy gates on the TLS fingerprint
and rate-limits per connection, so an exit IP buys nothing here. The input is still there if your
network needs one.

**Which grades come back?** All six GasBuddy reports — Regular (87), Midgrade (89), Premium
(91–93), Diesel, E85 and UNL88 (E15) — per station, in one pass. The `fuel` input only sets which
stations GasBuddy ranks and returns first.

**Does it work in Canada?** Yes. Canadian stations come back with `countryCode: "CA"`,
`currency: "CAD"` and `priceUnit: "cents_per_liter"`.

**Can I scrape many cities at once?** Yes — that is the point. Put every ZIP and city in `search`;
one run covers them all and de-duplicates by station id.

**Why did my ZIP return nothing?** A ZIP search covers only stations inside that ZIP. See the note
under Input.

**How fresh are the prices?** Each observation carries its own `postedTime`, and every row carries
`priceAgeHours` for its newest price. Use `maxPriceAgeHours` to keep only recent ones — it filters
on the time actually read from the station, not on GasBuddy's own `maxAge` parameter, which their
API accepts and does not apply.

***

*Public, non-personal listing data only: station prices, locations and brands. Member and account
pages, which `robots.txt` disallows, are not touched.*

# Actor input Schema

## `search` (type: `array`):

Locations to scrape — US ZIP codes, Canadian cities, city names, or "city, ST". One run covers every entry. Same field name the incumbent uses, so migration is drop-in. Note that a ZIP search returns only stations inside that ZIP boundary, which for a dense urban ZIP can legitimately be zero — use "city, ST" for a wider net.

## `fuel` (type: `string`):

Which grade drives GasBuddy's ranking and which stations it returns. Every grade a station reports is ALWAYS returned in the `prices` object regardless of this setting — there is no need to run once per grade.

## `brand` (type: `string`):

Only return stations of one brand, e.g. "Costco", "Shell", "Circle K". Matched against GasBuddy's own brand table, which is read at run time, so new chains work without an update. Leave empty for every brand. Not available from the incumbent.

## `maxPriceAgeHours` (type: `integer`):

Drop stations whose newest price is older than this many hours. Filtered on the posted time actually read from each station, not on GasBuddy's own `maxAge` parameter — that one is accepted by their API and does not filter. Leave empty to keep every station. Skipped stations are not charged for. Not available from the incumbent.

## `includeStationsWithoutPrices` (type: `boolean`):

GasBuddy lists stations nobody has posted a price for. They are dropped by default — in a fuel-price run they are a charge for no price — and are never charged for when dropped. Turn this on to use the actor as a station registry (name, brand, address, phone, coordinates, rating) instead.

## `lat` (type: `number`):

Optional latitude to search by coordinate instead of, or in addition to, a text search. Provide both lat and lng. A coordinate search also fills the `distance` field on every row.

## `lng` (type: `number`):

Optional longitude to search by coordinate. Provide both lat and lng.

## `maxStationsPerSearch` (type: `integer`):

Upper bound on stations collected for each search location, across cursor pages.

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

Cap on how many cursor pages to walk per search location. GasBuddy returns roughly 20-25 stations per page, ordered by proximity, so more pages means a wider radius.

## `maxItems` (type: `integer`):

Hard cap on total dataset rows (stations) across all locations. Stations seen in more than one location are de-duplicated and counted once.

## `maxRetries` (type: `integer`):

How many times to retry a failed request, each on a fresh connection. GasBuddy rate-limits per connection, so a rotation is free and usually enough.

## `maxRequestsPerMinute` (type: `integer`):

Politeness ceiling on requests to gasbuddy.com. One request returns 20-25 stations.

## `maxConcurrency` (type: `integer`):

How many requests may be in flight at once.

## `proxyConfiguration` (type: `object`):

Optional. This actor does NOT need a proxy: gasbuddy.com gates on the TLS fingerprint rather than the exit IP, and its rate limit is counted per connection, which a free reconnect resets. Leave this off unless you have a reason — a proxy adds cost and no measured benefit. Also accepted under the incumbent's field name `proxy`.

## Actor input object example

```json
{
  "search": [
    "90210",
    "Austin, TX"
  ],
  "fuel": "Regular",
  "brand": "Costco",
  "maxPriceAgeHours": 24,
  "includeStationsWithoutPrices": false,
  "maxStationsPerSearch": 100,
  "maxPages": 10,
  "maxItems": 1000,
  "maxRetries": 5,
  "maxRequestsPerMinute": 60,
  "maxConcurrency": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `stations` (type: `string`):

All station records. Append ?format=csv for CSV.

## `datasetUrl` (type: `string`):

The default dataset.

# 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 = {
    "search": [
        "90210",
        "Austin, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("diopside/gasbuddy-prices").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 = { "search": [
        "90210",
        "Austin, TX",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("diopside/gasbuddy-prices").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 '{
  "search": [
    "90210",
    "Austin, TX"
  ]
}' |
apify call diopside/gasbuddy-prices --silent --output-dataset

```

## MCP server setup

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

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/ue1wQQzz4UukjkgaP/builds/9tu8So8n2tBlhNlnC/openapi.json
