WAC/AMP Spread Calculation

The spread between two published drug-pricing benchmarks is the arithmetic that decides how much rebate value a claim actually carries, and getting it wrong by a rounding step compounds across millions of claims into a material reconciliation gap. A single adjudicated line names one drug through its 407-D7 Product/Service ID (NDC), but that drug simultaneously carries a Wholesale Acquisition Cost, an Average Manufacturer Price, an Average Wholesale Price, a National Average Drug Acquisition Cost, and — for the rebate math — a manufacturer best price. Each is published by a different source on a different cadence, denominated in a different unit of measure, and the differences between them are what drive rebate accrual, spread-pricing transparency reporting, and the reconciliation of what the plan pays against the 409-D9 Ingredient Cost the pharmacy was actually reimbursed. This section specifies how to compute those spreads as a decimal-exact, version-stamped, auditable subsystem that sits alongside the adjudication path within Rebate Calculation & Accrual Tracking.

Problem Framing

Rebate value is never read directly off a claim; it is derived from the distance between benchmarks. A manufacturer rebate contract might pay a percentage of WAC, a flat per-unit amount, or a formula anchored to AMP, and the plan’s spread-pricing exposure is the difference between the ingredient cost the sponsor is billed and the 409-D9 Ingredient Cost the pharmacy received. Both quantities are spreads, and both are computed from the same benchmark snapshot resolved against the same NDC. The subsystem therefore has one job: given a normalized NDC, a dispensing timestamp, and a 442-E7 Quantity Dispensed, return the exact per-unit and per-claim spread between any two named benchmarks, with the pricing snapshot version stamped on every result so the figure can be replayed during a rebate dispute or a payer audit.

The hazard is that pricing benchmarks look like simple numbers and invite floating-point handling. They are not. A WAC of 1249.99 per package, divided across a 30-unit dispense and multiplied back by a fractional quantity, accumulates binary-float error that surfaces as sub-cent drift — and sub-cent drift across a quarter of accrual reconciles to a variance line an auditor will ask about. Every figure here is decimal.Decimal, quantized explicitly with ROUND_HALF_UP, and the order of division and multiplication is fixed so the same inputs always yield the same cents. The exact rounding discipline is specified in the child guide, Calculating WAC/AMP Spreads with Decimal Precision; this page defines the benchmarks, their sources, the spread model, a reference calculator, and the failure taxonomy that keeps it correct.

Prerequisites

The spread calculator is a downstream consumer of pricing data and a resolved taxonomy. Before it runs, the following must be in place:

  • A resolved GPI, not a raw NDC. Benchmark files key on NDC, but rebate contracts and therapeutic grouping reason over GPI. The inbound 407-D7 NDC is normalized and mapped through the NDC-to-GPI Crosswalk Automation pipeline so that a spread computed for one package NDC attributes to the same therapeutic class as its equivalents. The calculator accepts the resolved GPI plus the canonical 11-digit NDC used for benchmark lookup.
  • Version-stamped benchmark feeds, ingested and dated. WAC and AWP arrive from a pricing compendium (First Databank, Medi-Span), NADAC and AMP-derived figures from CMS, each on its own cadence — NADAC weekly, AMP quarterly, WAC on manufacturer change notices. Each feed is ingested as an immutable, dated snapshot (for example pricing.version = "2026-07-13"), never a mutable table, so the benchmark active at the dispensing timestamp is the one used, exactly as required by Manufacturer Rebate Contract Modeling.
  • NDC pricing snapshots keyed by effective date. A single NDC carries a time series of WAC values as the manufacturer revises list price. The snapshot must resolve (NDC, service_date) to the WAC in effect on that date, not the current WAC — pricing an old claim at today’s WAC is the single most common spread defect.
  • Unit-of-measure normalization. Benchmarks are published per billing unit — each, mL, or gram — and the published unit is not always the 442-E7 Quantity Dispensed unit. A benchmark of 12.40 per mL against a quantity dispensed in “each” is a category error that produces a spread off by orders of magnitude. Every benchmark carries an explicit unit; the calculator refuses to subtract two benchmarks whose units disagree.
  • Library baseline. Python 3.11+, pydantic>=2.6 for strict pricing contracts, the standard-library decimal module for every money-bearing field, and structlog>=24.1 or stdlib logging for JSON telemetry. Benchmark amounts cross the wire as strings and are parsed straight into Decimal — never through float.
  • An append-only audit sink. Every spread computation — the two benchmarks, their versions, the units, and the resulting per-unit and per-claim figures — is serialized to the same event store described in Security & Compliance Boundaries for Claims Data before the value is used downstream.

The Pricing Benchmarks

Five benchmarks matter to the spread math, and confusing their sources or roles is where spread logic silently goes wrong. Each is a published or derived per-unit price with a distinct definition, publisher, and cadence.

Benchmark Source / publisher What it represents Cadence Role in spread math
WAC — Wholesale Acquisition Cost Manufacturer list price, distributed via FDB / Medi-Span compendia Manufacturer’s published list price to wholesalers, before rebates or discounts On manufacturer change notice Baseline list anchor; many rebate formulas and reimbursement caps are % of WAC
AWP — Average Wholesale Price Compendium-published, historically ≈ WAC × 1.20 Legacy “sticker” benchmark for reimbursement Tracks WAC changes Legacy reimbursement basis; AWP-minus-percentage contracts and the AWP→ingredient-cost spread
NADAC — National Average Drug Acquisition Cost CMS retail pharmacy invoice survey Actual average acquisition cost at retail pharmacies Weekly Transparency reference; the NADAC-to-409-D9 spread quantifies over/under-reimbursement
AMP — Average Manufacturer Price CMS, defined under the Medicaid Drug Rebate Program (SSA §1927) Average price wholesalers/pharmacies pay manufacturers for retail-class drugs Quarterly (monthly for some) Rebate base; the Medicaid unit rebate amount is computed from AMP and best price
Best Price Manufacturer-reported to CMS, with statutory exclusions Lowest price available to any purchaser Quarterly With AMP, sets the Medicaid unit rebate: max(0.231 × AMP, AMP − best_price) for innovator drugs

Two structural facts govern how these combine. First, not every spread is meaningful. A WAC-minus-AWP spread is arithmetic but not economic; the economically load-bearing spreads are WAC-minus-NADAC (list versus real acquisition), AMP-minus-best-price (the Medicaid rebate driver), and the paid-versus-409-D9 spread that measures the plan’s spread-pricing exposure. Second, AMP and best price are federal Medicaid constructs, published quarterly and lagged; a commercial rebate model that leans on them must treat them as a slower-moving snapshot than WAC and reconcile the lag explicitly in Rebate Accrual Reconciliation. Authoritative definitions live in the CMS Medicaid Drug Rebate Program documentation at cms.gov and the NCPDP standards at standards.ncpdp.org.

Benchmark spread waterfall from WAC down to ingredient cost A vertical price ladder for a single NDC. WAC sits at the top as the manufacturer list anchor. AWP is derived above WAC as a legacy sticker benchmark. Descending below WAC are AMP, then NADAC, then the 409-D9 Ingredient Cost actually reimbursed to the pharmacy, with best price at the bottom. Bracketed spreads on the right label the economically meaningful differences: WAC minus NADAC as the list-to-acquisition spread, AMP minus best price as the Medicaid rebate driver, and paid minus 409-D9 as the plan spread-pricing exposure. AWP legacy sticker ≈ WAC × 1.20 WAC manufacturer list anchor AMP Medicaid rebate base (quarterly) NADAC real acquisition cost (weekly) 409-D9 Ingredient Cost reimbursed to pharmacy Best Price lowest price to any purchaser per-unit price (higher → lower) WAC − AMP contract spread AMP − best price Medicaid rebate driver NADAC − 409-D9 reimbursement variance

Figure: The per-unit benchmark ladder for one NDC — WAC anchors list price, descending to AMP, NADAC, and the reimbursed 409-D9 Ingredient Cost, with the economically load-bearing spreads bracketed on the right.

Spread Specification

A spread is defined as minuend − subtrahend, both resolved to the same benchmark snapshot, the same NDC, and the same unit of measure, expressed first per unit and then scaled to per claim by 442-E7 Quantity Dispensed. The specification below fixes what is legal to subtract and how the result is rounded.

Spread Minuend Subtrahend Per-unit meaning Per-claim scaling
List-to-acquisition WAC NADAC Gross margin embedded in list price × 442-E7, quantize to cents
Contract rebate base WAC AMP Basis for % of WAC or AMP-anchored rebate × 442-E7, quantize to cents
Medicaid rebate driver AMP best price Component of the unit rebate amount applied via URA formula, then × 442-E7
Spread-pricing exposure plan-billed ingredient cost 409-D9 Ingredient Cost What the sponsor paid above pharmacy reimbursement already per claim; quantize to cents

Three rules make the specification safe. First, unit agreement is mandatory — subtracting a per-mL WAC from a per-each NADAC is rejected, not silently computed. Second, per-unit rounding happens once, at the end — the calculator holds full Decimal precision through the subtraction and the quantity multiplication, and quantizes to cents only on the final per-claim figure, because quantizing the per-unit spread first and then multiplying by quantity re-introduces the rounding error the whole subsystem exists to prevent. Third, negative spreads are legal but flagged — NADAC above WAC (below-cost list) or best price above AMP happens with generics and must surface as a signed value with a negative_spread flag, never clamped to zero, so Rebate Accrual Reconciliation can see it.

Spread-Pricing Transparency & the Paid-vs-409-D9 Reconciliation

Spread pricing is a PBM contracting model in which the plan sponsor pays the PBM a per-claim ingredient cost that exceeds the 409-D9 Ingredient Cost the PBM actually reimbursed the dispensing pharmacy; the difference is PBM margin. A pass-through (transparent) contract, by contrast, bills the sponsor exactly the 409-D9 the pharmacy received, and PBM compensation is a disclosed administrative fee. Whichever model a contract uses, the sponsor’s transparency reporting obligation is the same: reconcile, per claim and in aggregate, the amount billed to the plan against the 409-D9 Ingredient Cost paid at the counter, and attribute the residual to spread, dispensing fee, or 431-DV Other Payer Amount.

The reconciliation is itself a spread, and it uses the same decimal discipline as the benchmark math. For each adjudicated line the subsystem holds two figures: plan_billed_ingredient_cost and the 409-D9 the pharmacy was paid. The per-claim spread-pricing exposure is plan_billed − 409-D9, quantized once to cents, signed so that an overpaid claim (billed above paid) is positive and a pass-through claim reconciles to 0.00. Aggregated over a reporting period and grouped by GPI, this exposure is the number that appears in a transparency report and the number an auditor recomputes from the raw claim extract. Because the figure is derived, not stored, it must be reproducible: the same claim, the same pricing_version, and the same rounding mode must always yield the same cents, which is why the reconciliation runs over the same stateless, version-stamped calculator rather than an ad-hoc report query. The paid-side inputs — 409-D9 Ingredient Cost and 412-DC Dispensing Fee — come from the adjudicated response, tokenized of PHI upstream, so the reconciliation never touches 302-C2 Cardholder ID or the raw transaction. Where the billed and paid sides disagree beyond a de-minimis tolerance, the line is routed to Rebate Accrual Reconciliation as a variance to be explained, not silently absorbed.

Reference Python Implementation

The calculator below enforces strict pricing contracts with Pydantic v2, refuses unit-mismatched subtraction, holds Decimal precision until the final quantization with ROUND_HALF_UP, and emits a structured audit event carrying both benchmark versions. It touches no PHI: benchmarks and quantities are not member data, and the only correlation key is the tokenized 402-D2 reference.

python
import json
import logging
from decimal import Decimal, ROUND_HALF_UP, getcontext
from datetime import datetime, timezone
from typing import Optional
from pydantic import BaseModel, ConfigDict, field_validator

# 28 digits of working precision keeps intermediate products exact before
# the single final quantization to cents.
getcontext().prec = 28

logging.basicConfig(format="%(message)s", level=logging.INFO)
logger = logging.getLogger("wac_amp_spread")

CENTS = Decimal("0.01")


class Benchmark(BaseModel):
    """One published per-unit price for a single NDC, unit-tagged and versioned."""
    model_config = ConfigDict(extra="forbid")

    name: str                 # WAC | AWP | AMP | NADAC | BEST_PRICE
    amount: Decimal           # per-unit price, parsed straight from string
    unit: str                 # each | mL | gram -- billing unit of measure
    ndc_407d7: str            # canonical 11-digit NDC used for lookup
    pricing_version: str      # dated snapshot id, e.g. "2026-07-13"

    @field_validator("amount", mode="before")
    @classmethod
    def _no_float_ingress(cls, v: object) -> Decimal:
        # Reject float at the door: Decimal(0.1) != Decimal("0.1").
        if isinstance(v, float):
            raise ValueError("benchmark amount must arrive as str/Decimal, not float")
        return Decimal(str(v))


class SpreadResult(BaseModel):
    minuend: str
    subtrahend: str
    unit: str
    per_unit_spread: Decimal      # full precision, unrounded
    per_claim_spread: Decimal     # quantized to cents, ROUND_HALF_UP
    quantity_442e7: Decimal
    negative_spread: bool
    pricing_versions: dict[str, str]
    computed_at: str


class SpreadCalculator:
    """Decimal-exact per-unit and per-claim benchmark spread engine."""

    def spread(
        self,
        minuend: Benchmark,
        subtrahend: Benchmark,
        quantity_442e7: Decimal,   # 442-E7 Quantity Dispensed, Decimal never float
        claim_ref: str,            # tokenized 402-D2 reference, non-PHI
    ) -> SpreadResult:
        # Guard 1: refuse to subtract across mismatched units of measure.
        if minuend.unit != subtrahend.unit:
            raise ValueError(
                f"unit mismatch: {minuend.name}={minuend.unit} "
                f"vs {subtrahend.name}={subtrahend.unit}"
            )
        # Guard 2: both benchmarks must describe the same product.
        if minuend.ndc_407d7 != subtrahend.ndc_407d7:
            raise ValueError("benchmarks reference different NDCs")

        # Full-precision subtraction; NO intermediate quantization.
        per_unit = minuend.amount - subtrahend.amount

        # Scale to the claim, THEN quantize exactly once.
        per_claim = (per_unit * quantity_442e7).quantize(
            CENTS, rounding=ROUND_HALF_UP
        )

        result = SpreadResult(
            minuend=minuend.name,
            subtrahend=subtrahend.name,
            unit=minuend.unit,
            per_unit_spread=per_unit,
            per_claim_spread=per_claim,
            quantity_442e7=quantity_442e7,
            negative_spread=per_unit < 0,
            pricing_versions={
                minuend.name: minuend.pricing_version,
                subtrahend.name: subtrahend.pricing_version,
            },
            computed_at=datetime.now(timezone.utc).isoformat(),
        )
        self._audit(result, claim_ref)
        return result

    def _audit(self, result: SpreadResult, claim_ref: str) -> None:
        # Audit: benchmark identifiers, versions, and outcome -- no PHI.
        logger.info(json.dumps({
            "event": "spread_computed",
            "claim_ref": claim_ref,             # tokenized 402-D2, non-PHI
            "minuend": result.minuend,
            "subtrahend": result.subtrahend,
            "unit": result.unit,
            "per_claim_spread": str(result.per_claim_spread),
            "negative_spread": result.negative_spread,
            "pricing_versions": result.pricing_versions,
            "timestamp": result.computed_at,
        }))

The calculator is a pure function of (minuend, subtrahend, quantity) — no hidden state, no database call on the compute path — so it can be replicated across accrual workers and replayed against a historical pricing_version without side effects. Note that per_unit_spread is deliberately returned unrounded: any consumer that needs to re-scale it (for example a partial-fill adjustment) must apply its own quantity and quantize once, never chain a rounded per-unit figure into a second multiplication.

Engineering Constraints & Known Failure Modes

Spread math fails in a small set of recurring, diagnosable ways. Each has a deterministic rule rather than a silent coercion:

  • Unit-of-measure mismatch. A per-mL benchmark subtracted from a per-each benchmark is the highest-severity defect because it produces a plausible-looking but wildly wrong number. The unit guard raises before any arithmetic; upstream, unit normalization must reconcile the benchmark billing unit against the 442-E7 quantity unit, as detailed in Calculating WAC/AMP Spreads with Decimal Precision.
  • Stale benchmark against a back-dated claim. Pricing an old claim at today’s WAC over-states or under-states the spread. The snapshot resolver must key on (NDC, service_date) and return the WAC in effect on the dispensing date; a claim adjudicated for a prior quarter must reconcile against the AMP snapshot that was live that quarter, which lags real time by design.
  • Negative spreads clamped to zero. Generics routinely trade with NADAC above WAC or best price above AMP. Clamping the spread to zero hides a real economic condition and corrupts accrual. The calculator returns the signed value with negative_spread=True and lets reconciliation decide.
  • Float ingress from JSON. A benchmark deserialized from a JSON number arrives as float, and Decimal(0.1) is not Decimal("0.1"). The field_validator rejects float outright; feeds must serialize amounts as strings.
  • Rounding at the wrong step. Quantizing the per-unit spread before multiplying by quantity re-introduces error at claim scale. The single quantization point is the final per-claim figure — this ordering is the correctness core of the whole subsystem.
  • Snapshot skew across workers. Two accrual workers holding different pricing_version snapshots compute different spreads for the same claim. Every result stamps both benchmark versions; reconciliation and re-computation reconcile on those stamps, the same versioned-snapshot discipline used across Manufacturer Rebate Contract Modeling.

Performance & Correctness Tuning

  • Decimal precision, quantized once. Set getcontext().prec high enough (28 is ample) that intermediate products never lose digits, and quantize exactly one figure — the per-claim spread — with ROUND_HALF_UP. Never carry a float through any step of the pricing math.
  • Versioned pricing snapshots, loaded by date. Keep each benchmark feed resident as an immutable dated snapshot and resolve (NDC, service_date) into it. A cache miss is a pricing gap to be surfaced, never a reason to hit a database synchronously and risk a stale read on the accrual path.
  • Idempotent, replayable computation. Because the calculator is pure over (benchmarks, quantity), the tokenized 402-D2 reference plus the two pricing_version stamps form an idempotency key: a re-run during reconciliation re-derives the identical cents. This is what lets Rebate Accrual Reconciliation compare accrued against invoiced figures without ambiguity.
  • Batch concurrency over a stateless core. Quarterly accrual runs fan out across asyncio.gather or a worker pool over the same stateless calculator, matching the concurrency model of Asynchronous Batch Adjudication Workflows; no shared mutable state means no locking on the compute path.
  • Audit before use. Serialize the spread event — both benchmarks, both versions, the units, and the cents — to the append-only store before the figure flows into an accrual, so any rebate line can be reconstructed identically during a manufacturer dispute or a payer examination.

In This Section

  • Calculating WAC/AMP Spreads with Decimal Precision — the exact Decimal context, quantization order, banker’s-versus-half-up rounding choice, and unit normalization tied to 442-E7 Quantity Dispensed and 409-D9 Ingredient Cost that make the spread reproducible to the cent.

← Back to Rebate Calculation & Accrual Tracking