# US Airline Fare & Route-Market Analytics (BTS DB1B) (`malonestar/bts-db1b-fare-route-market-analytics`) Actor

Historical US airline fares and route economics by quarter, from the official BTS DB1B 10% ticket survey. Average and median fares, carrier market share and concentration (HHI), distance and estimated passengers by route, carrier or airport, with honest sample weighting. Not live booking prices.

- **URL**: https://apify.com/malonestar/bts-db1b-fare-route-market-analytics.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Travel, Developer tools, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.50 / 1,000 route fare-market 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

## US Airline Fare & Route-Market Analytics (BTS DB1B)

Historical **US airline fare economics and route-market analytics** — average and
median fares, carrier market share and concentration (HHI), nonstop share and
market distance — computed straight from the US Department of Transportation's
official **DB1B Origin & Destination Survey**, a 10% random sample of all US
airline tickets. Keyless, no scraping, no API key.

### This is a 10% HISTORICAL sample — NOT live booking prices

Read this before anything else, because it defines what the product is and is not:

- **Historical, not live.** DB1B is filed quarterly and published with a ~1-2
  quarter lag. This actor answers "what did people actually pay on this route
  last quarter, and who flew it" — not "what does a ticket cost to book today".
  If you need live booking prices, this is the wrong tool (and a different
  market entirely).
- **A 10% sample.** DB1B is a 10% random sample of tickets. Every passenger and
  volume figure this actor reports is a **sample count**; `estimated_market_passengers`
  is the sample multiplied by 10, and **every row carries `is_10pct_sample: true`
  and `sample_multiplier: 10`** so a sample count is never mistaken for a market
  total. Fares are actual prorated per-passenger dollar amounts (not sampled
  away), so a passenger-weighted average over the sample is a sound estimate of
  the market's average fare.
- **Why nobody else on the Store sells this.** Every "airline fare" actor on the
  Store scrapes live booking sites (Google Flights, Skiplagged, etc.) — a
  different product for a different buyer. The historical, cause-of-market,
  fare-distribution rollup from the government's own O\&D survey is uncontested.

### Who this is for

- **Route planners & network analysts:** which carriers fly a market, their
  passenger share, the average/median fare, and how concentrated (HHI) the
  market is — before proposing a new route or a schedule change.
- **Airline competitive-intelligence teams:** benchmark a competitor's fares and
  share on any city pair, quarter by quarter.
- **Aviation & management consultants:** market-sizing (estimated passengers),
  fare trends and concentration for diligence and strategy decks, from the
  authoritative federal source.
- **Airport authorities & economic-development offices:** measure fares, carrier
  mix and nonstop connectivity for your airport's markets to support air-service
  development cases.
- **Fare-trend journalists & researchers:** "how much did LAX-JFK cost last
  quarter", "which carrier dominates DEN", "average fare on connecting vs
  nonstop markets" — one run, official numbers, with the fare distribution.
- **AI agents (MCP):** a clean, typed, chainable fare/route-market tool over a
  multi-million-row quarterly federal dataset that agents cannot practically
  download and parse themselves (each quarter is a ~100 MB zip holding a ~1 GB
  CSV of 7M+ rows).

### What one run costs — read this first

Pricing is **pay-per-result**: one dataset row = one (group x quarter) rollup.
At **$10 per 1,000 rows** a row costs **$0.01**.

| Run | Typical rows | Typical cost |
|---|---|---|
| One route, one quarter (`origins:["LAX"], dests:["JFK"]`) | 1 | ~$0.01 |
| One airport's routes, one quarter (`origins:["LAX"]`) | ~300-600 | ~$3-6 (cap with `maxResults`) |
| One carrier's routes, one quarter (`carriers:["WN"], groupBy:"carrier_route"`) | ~600 | ~$6 (capped) |
| City-market pairs for one metro, one quarter (`groupBy:"market"`) | ~200-400 | ~$2-4 |
| Default prefill (LAX origin routes, 2025-Q2, maxResults 500) | up to 500 | up to $5.00 |

`maxResults` (default **500**) is a **hard billing cap** enforced in one audited
code path for every grouping mode. When it bites, rows are dropped
smallest-groups-first and **every emitted row carries `results_truncated: true`** —
a capped answer is never passed off as complete. A run that cannot answer (bad
input, BTS outage, missing quarter without `allowPartial`) **fails loudly and
bills nothing**.

A single run is capped at **4 quarters** (each quarter is a ~100 MB, ~7-million-row
download from BTS). Split longer histories across runs. An unfiltered, all-routes
run over a full quarter aggregates tens of thousands of markets and is
memory-heavy — an `origins`/`dests`/`carriers` filter keeps it light and cheap.

### Example input

```json
{
  "sinceQuarter": "2025-Q1",
  "untilQuarter": "2025-Q1",
  "origins": ["LAX"],
  "groupBy": "route",
  "minPassengers": 10,
  "maxResults": 500
}
```

Or a single city pair, all carriers, grouped by carrier:

```json
{
  "sinceQuarter": "2024-Q3",
  "untilQuarter": "2025-Q2",
  "origins": ["DEN"],
  "dests": ["ORD"],
  "groupBy": "carrier_route",
  "minPassengers": 10,
  "maxResults": 100
}
```

Leave `sinceQuarter`/`untilQuarter` blank and the run defaults to the two most
recent published quarters — **discovered live**: DB1B publishes with a ~1-2
quarter lag, and the actor probes backward from the current quarter until a file
answers. The quarter it lands on is reported on every row as
`latest_quarter_available`.

### Grouping modes

| `groupBy` | One row per | Use it for |
|---|---|---|
| `route` (default) | directional origin-dest x quarter, across carriers | "How is LAX-JFK overall — fare, share, HHI?" |
| `carrier_route` | ticketing carrier x origin-dest x quarter | Carrier-level fare benchmarking on a route |
| `origin_airport` | origin airport x quarter (all outbound markets) | Airport-level fares and connectivity |
| `market` | origin city-market x dest city-market x quarter | Metro-to-metro analysis (all NYC airports as one) |

Markets are **directional** in DB1B, so LAX-JFK and JFK-LAX are separate rows.
Carrier market-share and HHI fields are meaningful for `route`, `origin_airport`
and `market` (which span carriers) and trivial for `carrier_route` (one carrier
per group).

### Output fields (the important ones)

Every row is one (group x quarter). All fields are nullable and documented in the
dataset schema; the key ones:

- **Identity:** `group_by`, `quarter`, `year`, `quarter_num`, `carrier`,
  `origin`, `dest`, `route`, `origin_city_market_id`, `dest_city_market_id`,
  `city_market`, `carriers_included`
- **Volume (10% sample):** `sample_ticket_count`, `sample_passengers`,
  `estimated_market_passengers` (x10), `is_10pct_sample`, `sample_multiplier`
- **Fares (cleaned, passenger-weighted):** `avg_fare`, `median_fare`,
  `fare_p25`, `fare_p75`, `fare_min`, `fare_max`, `fare_basis`,
  `fare_sample_ticket_count` / `fare_sample_passengers` (the clean denominator),
  `bulk_fare_ticket_count`, `zero_or_missing_fare_ticket_count`
- **Route structure:** `nonstop_sample_passengers`,
  `connecting_sample_passengers`, `nonstop_pax_share_pct`,
  `avg_market_distance_miles`
- **Carrier concentration:** `carrier_count`, `top_carrier`,
  `top_carrier_share_pct`, `market_hhi`, `market_concentration`
  (DOJ/FTC band)
- **Provenance & completeness:** `quarters_requested`, `quarters_fetched`,
  `quarters_missing`, `data_complete`, `quarter_rows_scanned`,
  `groups_suppressed_below_min_passengers`, `results_truncated`,
  `latest_quarter_available`, `data_source` (the exact BTS zip URL),
  `data_vintage` (the file's HTTP Last-Modified — BTS revises files in place),
  `retrieved_at`

### How the numbers stay honest

This dataset has traps, and the actor is built around them rather than on top of
them:

- **Fares are cleaned, and the cleaning is disclosed on every row.** A fare
  enters the fare statistics (`avg_fare`/`median_fare`/`fare_p25`/`fare_p75`/
  `fare_min`/`fare_max`) **only if it is not a bulk fare (`BulkFare != 1`) and is
  a positive dollar amount (`MktFare > 0`)**. Bulk fares (negotiated/tour tickets)
  and $0/blank fares (frequent-flyer, non-revenue) are **counted in volume but
  excluded from every fare statistic** — a naive `Number(x) || 0` would fabricate
  $0 fares and drag every average toward zero. `bulk_fare_ticket_count` and
  `zero_or_missing_fare_ticket_count` disclose exactly what was excluded, and
  `fare_sample_passengers` is the passenger weight actually behind the averages.
- **Averages are passenger-weighted, and median is the robust measure.** DB1B has
  legitimately tiny fares (measured min $0.15) and rare very high fares (max
  $30,656) that skew a mean; `avg_fare` is passenger-weighted, but `median_fare`
  and the p25/p75 quartiles (from a $5-resolution passenger-weighted histogram)
  are the robust central measures, and `fare_min`/`fare_max` are published exactly
  so the distribution is visible. Tails are **not** silently trimmed.
- **Passenger counts are sample counts, and the estimate is labelled.** DB1B is a
  10% sample; `estimated_market_passengers` is `sample_passengers x 10` and every
  row says `is_10pct_sample: true`. A sample count is never presented as absolute
  traffic.
- **Market share is by TICKETING carrier.** Share and HHI are attributed to the
  carrier that sold the ticket (`TkCarrier`); the operating carrier differs on
  \~27% of rows (codeshare/regional), so this is the honest "who sells the market"
  measure. Foreign carriers on US-endpoint itineraries and the DB1B `--`
  placeholder appear as their own codes — never guessed or merged.
- **A live drift gate runs before anything billable**, per quarter: the real
  header must contain every column this actor reads (a renamed column fails the
  run, never emits nulls); the file must hold >=300,000 rows, >=15 ticketing
  carriers and >=100 origin airports; at least one pinned mega-hub
  (ATL/ORD/DFW/DEN/LAX) must appear as an origin; the file's own Year/Quarter
  columns must match the quarter requested (a stale or mislabeled file fails
  instead of billing); a negative-control carrier code must match zero rows; and
  the overall clean passenger-weighted mean fare must fall in a sane $40-$1500
  band (a cents-vs-dollars units error or the wrong fare column fails here).
- **A missing quarter never silently vanishes.** If BTS hasn't published a
  requested quarter (or the download fails), the run FAILS by default with the
  per-quarter status. Opt in with `allowPartial: true` and every row then reports
  `quarters_missing` and `data_complete: false`.
- **Cheap trap coverage:** the BTS 404 page is HTML at a real 404; a maintenance
  page served at HTTP 200 is detected by byte shape (a zip must start with `PK`)
  and treated as an outage, never parsed. The quarter is probed with a ranged
  GET, not HEAD (federal hosts have answered 404 to HEAD on URLs they serve to
  GET). The CSV entry is selected by extension.

### Use as an MCP tool

This actor works as a chainable MCP tool via `https://mcp.apify.com` — the input
schema is fully described, quarter strings are validated with actionable error
messages (an agent sending `"2025-05"`, `"Q2 2025"` or `"2025-Q9"` gets a named
reason, not a silent empty dataset), and a filter that matches nothing fails with
the list of carrier codes actually present in the window. Billing is unchanged
when called as a tool; a run that cannot answer fails without billing.

Example agent prompt: *"Using the BTS DB1B fare & route-market tool, find the
average and median fare and each carrier's market share on Denver-Chicago for the
last two published quarters, grouped by carrier\_route."*

### FAQ

**How fresh is the data?** DB1B is quarterly and lags ~1-2 quarters. The run
discovers the latest published quarter live and reports it on every row as
`latest_quarter_available`. BTS also revises files in place — `data_vintage`
carries each file's Last-Modified.

**Is this live booking prices?** No. It is the historical DB1B *fare survey* —
what passengers actually paid, filed quarterly. It is the right tool for fare
economics and market analysis, the wrong tool for booking a flight.

**Why is this a "sample"?** DB1B is DOT's 10% random sample of all US airline
tickets. Volume figures are sample counts; multiply by 10 (done for you in
`estimated_market_passengers`) for a market estimate. Fares are real dollar
amounts, so fare averages are sound estimates.

**Which carriers are covered?** All carriers appearing in the survey — ~37
ticketing-carrier codes in a recent quarter, including foreign carriers on
itineraries with a US endpoint. Share/fares use the ticketing carrier.

**Why do bulk and $0 fares not count in the average?** Bulk fares are negotiated
tour-operator tickets and $0 fares are frequent-flyer/non-revenue — neither
represents a published fare a traveler would pay, so they are excluded from fare
statistics but still counted in volume. Every row discloses how many were
excluded.

**Can I get years of history?** Yes, 4 quarters per run (each quarter is a
\~100 MB federal download). Loop runs across windows; the DB1B O\&D survey goes
back to 1993-Q1.

**What does the HHI mean?** The Herfindahl-Hirschman Index of ticketing-carrier
passenger shares in the market (0-10,000). `market_concentration` bins it into
the DOJ/FTC bands: unconcentrated (<1500), moderately concentrated (1500-2500),
highly concentrated (>2500). A monopoly route reads ~10,000.

### Data source

US DOT Bureau of Transportation Statistics, TranStats **Airline Origin &
Destination Survey (DB1B)**, DB1BMarket quarterly PREZIP bulk files at
`transtats.bts.gov/PREZIP/` — public domain, keyless. Each output row names the
exact source zip (`data_source`) and its Last-Modified (`data_vintage`).

# Actor input Schema

## `sinceQuarter` (type: `string`):

First quarter of BTS DB1B data to include, as YYYY-Qn (e.g. "2025-Q1") or YYYY-n (e.g. "2025-1"), n being 1-4. Leave blank to default to the quarter before the latest published quarter. The DB1B Origin & Destination Survey begins 1993-Q1 and is published with a ~1-2 quarter lag; the run discovers the latest available quarter live and reports it on every row. An unusable value (a month form, slash date, bare year, or quarter 5) FAILS the run loudly — it is never silently ignored.

## `untilQuarter` (type: `string`):

Last quarter of BTS DB1B data to include, as YYYY-Qn or YYYY-n. Leave blank to default to the latest published quarter. A window spanning more than 4 quarters is rejected (each quarter is a ~100 MB, ~7-million-row BTS download). A quarter BTS has not yet published is reported as missing — with allowPartial false (the default) the run fails loudly rather than quietly narrowing your window.

## `origins` (type: `array`):

Optional. IATA origin airport codes, e.g. \["LAX","JFK"]. Leave empty for all origins. Markets are DIRECTIONAL in DB1B, so LAX-JFK and JFK-LAX are separate rows. An unfiltered, all-routes run over a full quarter aggregates tens of thousands of markets and is memory-heavy; a filter keeps it light.

## `dests` (type: `array`):

Optional. IATA destination airport codes, e.g. \["JFK","ORD"]. Leave empty for all destinations.

## `carriers` (type: `array`):

Optional. BTS ticketing-carrier codes to include, e.g. \["WN","AA","DL","UA","B6"]. Market share and fares are attributed to the TICKETING carrier (who sold the ticket); the operating carrier differs on ~27% of rows (codeshare/regional). Leave empty for all carriers (DB1B includes ~37 codes, incl. foreign carriers on US-endpoint itineraries and a "--" placeholder). A code matching no rows fails the run with the list of carriers actually present — never a silent empty dataset.

## `groupBy` (type: `string`):

How to roll markets up. "route" = one row per (directional origin-dest x quarter) across carriers; "carrier\_route" = one row per (ticketing carrier x origin-dest x quarter); "origin\_airport" = one row per (origin airport x quarter, all its outbound markets); "market" = one row per (origin city-market x dest city-market x quarter), which groups multi-airport metros (e.g. all NYC airports together). Carrier market-share/HHI fields are meaningful for route/origin\_airport/market (which span carriers) and trivial for carrier\_route. Omitted = "route".

## `minPassengers` (type: `integer`):

Suppress groups with fewer than this many SAMPLE passengers in a quarter (noise control — a market with 2 sampled passengers gives a meaningless average fare). Remember DB1B is a 10% sample, so 10 sample passengers ~ 100 estimated. Suppressed-group count is disclosed on every emitted row as groups\_suppressed\_below\_min\_passengers. Set 0 to keep everything. Omitted = 10.

## `maxResults` (type: `integer`):

Hard cap on emitted rows — each row is one billed result. Enforced by a single audited code path for every groupBy mode; when the cap bites, rows are dropped smallest-groups-first and every emitted row carries results\_truncated=true. Omitted = 500.

## `allowPartial` (type: `boolean`):

If a requested quarter cannot be retrieved (not yet published by BTS, or the download failed), the run FAILS loudly by default so a partial window is never mistaken for a complete one. Set true to proceed with the quarters that did arrive — every row then reports quarters\_missing and data\_complete=false.

## Actor input object example

```json
{
  "sinceQuarter": "2025-Q2",
  "untilQuarter": "2025-Q2",
  "origins": [
    "LAX"
  ],
  "groupBy": "route",
  "minPassengers": 10,
  "maxResults": 500,
  "allowPartial": false
}
```

# Actor output Schema

## `results` (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 = {
    "sinceQuarter": "2025-Q2",
    "untilQuarter": "2025-Q2",
    "origins": [
        "LAX"
    ],
    "groupBy": "route",
    "minPassengers": 10,
    "maxResults": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/bts-db1b-fare-route-market-analytics").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 = {
    "sinceQuarter": "2025-Q2",
    "untilQuarter": "2025-Q2",
    "origins": ["LAX"],
    "groupBy": "route",
    "minPassengers": 10,
    "maxResults": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("malonestar/bts-db1b-fare-route-market-analytics").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 '{
  "sinceQuarter": "2025-Q2",
  "untilQuarter": "2025-Q2",
  "origins": [
    "LAX"
  ],
  "groupBy": "route",
  "minPassengers": 10,
  "maxResults": 500
}' |
apify call malonestar/bts-db1b-fare-route-market-analytics --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,malonestar/bts-db1b-fare-route-market-analytics"
        }
    }
}

```

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/tUlwgdcod6OYg0hf3/builds/bxC5g0a3zwZoEvCNo/openapi.json
