# NADAC Drug Pricing API — Weekly NDC Price Movers (`malonestar/nadac-price-movers`) Actor

Drug pricing API built on CMS/Medicaid NADAC weekly data: per-NDC price time series, week-over-week deltas, and biggest-mover spike/drop detection. Filter by NDC or drug name over any date window to track generic drug price changes. Keyless, official source.

- **URL**: https://apify.com/malonestar/nadac-price-movers.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Developer tools, Automation, AI
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.50 / 1,000 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/platform/actors/running/actors-in-store#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

## NADAC Drug Pricing API — Weekly NDC Price Movers

Turn the CMS/Medicaid **NADAC** weekly drug-price file into a per-NDC time series with
real price-change deltas and a biggest-mover ranking. Keyless, official source, no
scraping.

Most NADAC actors on the Store dump the current week's prices. This one stitches the
weekly publications together, collapses them into **actual price points**, and tells you
what moved, by how much, and how long the current price has been in force.

***

### Who this is for

- **PBMs and pharmacy analytics teams** — catch a generic spiking before it shows up in
  a reimbursement dispute.
- **340B TPAs and covered entities** — track acquisition-cost drift on the drugs you
  dispense most.
- **Formulary and rebate teams** — brand-vs-generic per-unit spread comes back on the
  same row.
- **State Medicaid / MCO rate setters** — NADAC is the benchmark; this is the delta.
- **AI agents** — see "Use as an MCP tool" below.

***

### How NADAC actually works (and why this matters for your query)

A NADAC yearly dataset is the **stack of that year's weekly publications**:

| Column | What it is |
|---|---|
| `as_of_date` | the weekly publication date — 30 snapshots in 2026 so far |
| `effective_date` | the date the **current price took effect**, which **repeats** in every weekly snapshot until the price actually changes |

Live example, NDC `00093505610` (ATORVASTATIN 10 MG TABLET):

```
as_of 2026-06-17 .. 2026-07-15  ->  effective_date 2026-06-17, price 0.02452   (5 snapshots)
as_of 2026-07-22 .. 2026-07-29  ->  effective_date 2026-07-22, price 0.02403   (2 snapshots)
```

So "the last two rows" are usually **the same price seen twice**. This Actor collapses
each series to distinct effective dates first (the newest publication wins if CMS
restates a price), then computes the delta. `pct_change` for that NDC is **−2.00%**,
not 0.00%.

> **Corrected in v1.1.** Version 1.0 compared the raw last two rows. Measured on this
> Actor's own default input on 2026-08-01, that reported **273 of 278 NDC series (98.2%)
> as a 0.00% flat move**, and zero as having moved at all, when the truth was 265 real
> movers and 13 genuinely flat. If you pulled data before 2026-08-01, re-run it.

***

### Example input

```json
{
  "drugName": "atorvastatin",
  "daysBack": 180,
  "minPctChange": 0,
  "direction": "all",
  "maxResults": 50
}
```

Other useful shapes:

| Goal | Input |
|---|---|
| One NDC's full history | `{ "ndc": "0093-5056-10", "daysBack": 365 }` — hyphens, spaces and dropped leading zeros are all accepted |
| Spikes only | `{ "drugName": "metformin", "minPctChange": 10, "direction": "up" }` |
| Brand-vs-generic spread | `{ "drugName": "abilify", "daysBack": 180 }` — populates `corresponding_generic_drug_nadac_per_unit` and `brand_generic_spread_pct` |
| Multi-year window | `{ "drugName": "insulin", "sinceDate": "2024-01-01" }` — spans the yearly datasets automatically |

Leaving both `ndc` and `drugName` empty scans every NDC in the window (~700,000 weekly
rows over 180 days). That run may hit the page or time budget, and if it does **every
emitted row will say so** via `data_complete: false`.

***

### Output fields

One row per NDC, ranked by biggest absolute percent move.

#### Drug identity

| Field | Description |
|---|---|
| `ndc` | 11-digit National Drug Code. |
| `ndc_description` | Drug name/description as published in NADAC. |
| `pricing_unit` | Unit the per-unit price is expressed in (EA, ML, GM, ...). |
| `classification_for_rate_setting` | NADAC brand/generic classification (G = generic, B = brand). |
| `otc` | Over-the-counter indicator (Y/N). |
| `pharmacy_type_indicator` | Pharmacy type the survey covers (e.g. `C/I` = chain and independent). |
| `explanation_code` | NADAC explanation code for how the rate was derived. |

#### The price move

| Field | Description |
|---|---|
| `latest_price` | Most recent NADAC per-unit price in the window. |
| `latest_date` | Effective date of the latest price. |
| `previous_price` | The **previous distinct** per-unit price — never a duplicate of the latest. |
| `previous_date` | Effective date of the previous price. Can never equal `latest_date`. |
| `abs_change` | `latest_price − previous_price`, in pricing units. |
| `pct_change` | Percent change between those two price points. Null when there is only one price point in the window, or the prior price was zero. |
| `direction` | `up`, `down`, `flat`, or `new` (a single price point). |
| `days_between_prices` | Calendar days between the previous and latest effective dates. |
| `prior_pct_change` | The change immediately **before** the latest one — tells a one-off correction from a sustained climb. |
| `cumulative_pct_change` | First price point in the window to the latest. |
| `volatility_pct` | Standard deviation of the point-to-point percent changes. Null with fewer than three price points. |

#### The series

| Field | Description |
|---|---|
| `weeks_of_history` | Number of weekly NADAC observations found for this NDC in the window. |
| `distinct_price_points` | Number of **distinct effective dates** — the count that drives the delta. Always ≤ `weeks_of_history`. |
| `price_changes_in_window` | How many times the price actually changed value. |
| `weeks_at_latest_price` | How many weekly publications have carried the current price so far. |
| `first_price` / `first_date` | Earliest price point in the window. |
| `min_price` / `min_price_date` | Lowest price observed, and when. |
| `max_price` / `max_price_date` | Highest price observed, and when. |

#### Brand vs generic

| Field | Description |
|---|---|
| `corresponding_generic_drug_nadac_per_unit` | For a **brand** record, the per-unit price of the matching generic. Null on a generic record (normal). Populate it with `{"drugName": "abilify"}`. |
| `corresponding_generic_drug_effective_date` | Effective date of that generic price. |
| `brand_generic_spread_pct` | How much more the brand costs per unit than its generic, as a percent of the generic price. |

#### Provenance and completeness — read these before you act on a ranking

| Field | Description |
|---|---|
| `as_of_date` | Newest NADAC **publication week** this NDC appears in. |
| `feed_as_of_date` | Newest publication week present in the source for this run. |
| `is_latest_published_week` | True when this NDC is in the current week. Null when the feed date could not be established. |
| `window_start` / `window_end` | The price-history window this row was computed over. |
| `data_complete` | `true` when every matching row CMS reported was retrieved. **`false`** when the fetch was truncated — the ranking is then over a subset. **`null`** when CMS reported no total, so completeness could not be established. Never silently `true`. |
| `ranking_scope` | Plain-English version of the above. |
| `truncation_note` | What limited the run (page cap, row cap, time budget, failed year). Null on a clean read. |
| `rows_retrieved` / `rows_reported_total` | What we hold vs what CMS said matches. |
| `datasets_queried` | Per-year outcome, as `year:status` pairs. |
| `datasets_failed` | Years whose fetch failed, with the error. **Null here is good news**, not a dead column. |
| `dataset_status` | `ok`, `partially_unavailable`, or `all_unavailable`. |
| `source_system` / `source_url` | Attribution. |

**`null` means "not checked". `false` means "checked, and negative".** If every yearly
dataset fails, the run **fails loudly and emits nothing** rather than telling you no
drugs moved.

***

### Live source-integrity checks

Before a single billable row is produced, the Actor runs **11 checks against the live
CMS datastore** and fails the run if any regresses. Offline tests cannot catch a
silently degraded upstream — the fixtures were shaped from the degraded data.

| Check | What it protects against |
|---|---|
| `yearly_datasets_discovered` | CMS renaming the yearly-table title format. |
| `requested_year_dataset_present` | The year covering your window having no dataset. |
| `required_columns_present` | A column rename silently emptying every price. |
| `sort_parameter_applied` | DKAN accepts an unapplied `sort` parameter, echoes it back verbatim and returns HTTP 200. If it ever stops honouring `sorts`, pagination becomes unordered and a truncated fetch becomes an arbitrary subset. |
| `reported_total_present` | The dataset being replaced by a stub. |
| `feed_freshness_days` | CMS stopping publication — a stale feed silently answers last month's question. |
| `positive_canary_rows` / `_description` / `_price_band` | A known-good NDC (`00093505610`, ATORVASTATIN 10 MG TABLET) resolving to the right drug at a sane price. |
| `negative_control_empty` | A known-nonexistent NDC returning rows — which would mean the filter is no longer being applied. |
| `price_zero_sentinel_absent` | NADAC starting to publish `0.00000` as a null sentinel, which would fabricate −100% moves. |

Every measured value is logged on every run. If you hit what you believe is a false
positive, `skipDriftAssertions: true` runs anyway — rows still carry `dataset_status`.

***

### Pricing

Pay per result: **$0.01 per row** ($10 per 1,000), with graduated discounts on paid
Apify plans. One row = one NDC's complete price-movement record. Use `maxResults` to
cap spend, and `minPctChange` to pay only for drugs that actually moved.

***

### Use as an MCP tool

Available to AI agents via `mcp.apify.com`. Every input and output field carries a
description, so an agent can chain this cleanly:

> "Has the acquisition cost of metformin 500 mg moved more than 5% in the last quarter?"
> → `{ "drugName": "metformin 500", "daysBack": 90, "minPctChange": 5 }`

Agents should read `data_complete` and `dataset_status` before asserting a ranking.

***

### FAQ

**Why is `previous_date` weeks before `latest_date` instead of exactly one week?**
Because NADAC prices only change when they change. `previous_date` is the previous
**distinct** price point. `days_between_prices` and `weeks_at_latest_price` tell you
the gap.

**Why does `weeks_of_history` say 24 but `distinct_price_points` say 6?**
24 weekly publications carried this NDC; the price took 6 distinct values across them.
Both are true and both are useful.

**Why did my broad query come back with `data_complete: false`?**
An unfiltered 180-day window is roughly 700,000 weekly rows. The run hit a budget.
Narrow by `drugName`, `ndc`, or a shorter window — or accept the row's own statement
that the ranking is over a subset.

**Zero results for my NDC.** NADAC covers outpatient drugs reimbursed by Medicaid, and
codes retire. The Actor logs whether the query reached CMS, so a genuine empty answer is
distinguishable from an outage. Try a wider window or search by name.

**Is this the same as WAC or AWP?** No. NADAC is the survey-based **acquisition** cost
retail pharmacies actually pay. That is the point.

***

### Related Actors

- **Drug Shortage API — Delta Monitor & FDA Shortage Tracker** — openFDA shortage
  feed with new/resolved/reappeared deltas. Same buyer, adjacent question.
- **CMS Open Payments** and **Medicaid Exclusion Screener** — the rest of the CMS shelf.

***

*Data: CMS / Medicaid.gov NADAC (National Average Drug Acquisition Cost), a public,
keyless dataset. This Actor is a screening and monitoring tool, not a pricing
determination or reimbursement advice.*

# Actor input Schema

## `ndc` (type: `string`):

National Drug Code to track over time, e.g. 00093505610 (atorvastatin 10 mg tablet — verified live in NADAC). Hyphens, spaces and missing leading zeros are all fine: 0093-5056-10 and 93505610 resolve to the same code. Provide this and/or Drug name to focus the run.

## `drugName` (type: `string`):

Substring matched against the NDC description (case-insensitive), e.g. METFORMIN or CICLOPIROX. Builds a time series for every matching NDC.

## `daysBack` (type: `integer`):

Size of the look-back window in days ending today, used to build the price history. Ignored if 'Since date' is set.

## `sinceDate` (type: `string`):

Start of the price-history window as YYYY-MM-DD. Overrides 'Days back' when provided. Multi-year windows automatically span the yearly NADAC datasets.

## `minPctChange` (type: `integer`):

Only emit NDCs whose absolute week-over-week percent change is at least this value. Set 0 to emit every NDC with history.

## `direction` (type: `string`):

Which movers to keep: 'up' (price increases only), 'down' (price drops only), or 'all'.

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

Maximum number of NDC mover records to return, after ranking by biggest absolute percent move.

## `skipDriftAssertions` (type: `boolean`):

Diagnostic escape hatch. By default the Actor runs 11 live checks against the CMS datastore before emitting anything billable (columns present, sort actually applied, feed freshness, a known-good NDC returning a known price, a known-empty NDC returning nothing) and fails the run rather than ranking prices on a drifted source. Turn this on only if you believe a check is a false positive and you need the data anyway - every emitted row still carries dataset\_status and data\_complete.

## Actor input object example

```json
{
  "drugName": "atorvastatin",
  "daysBack": 180,
  "minPctChange": 0,
  "direction": "all",
  "maxResults": 50,
  "skipDriftAssertions": false
}
```

# Actor output Schema

## `results` (type: `string`):

Per-NDC drug price movers with week-over-week deltas, in 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 = {
    "drugName": "atorvastatin",
    "daysBack": 180,
    "direction": "all",
    "maxResults": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/nadac-price-movers").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 = {
    "drugName": "atorvastatin",
    "daysBack": 180,
    "direction": "all",
    "maxResults": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("malonestar/nadac-price-movers").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "drugName": "atorvastatin",
  "daysBack": 180,
  "direction": "all",
  "maxResults": 50
}' |
apify call malonestar/nadac-price-movers --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=malonestar/nadac-price-movers",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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