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-D7NDC 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-E7Quantity Dispensed unit. A benchmark of12.40per 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.6for strict pricing contracts, the standard-librarydecimalmodule for every money-bearing field, andstructlog>=24.1or stdlibloggingfor JSON telemetry. Benchmark amounts cross the wire as strings and are parsed straight intoDecimal— never throughfloat. - 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.
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.
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
unitguard raises before any arithmetic; upstream, unit normalization must reconcile the benchmark billing unit against the442-E7quantity 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=Trueand lets reconciliation decide. - Float ingress from JSON. A benchmark deserialized from a JSON number arrives as
float, andDecimal(0.1)is notDecimal("0.1"). Thefield_validatorrejects 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_versionsnapshots 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().prechigh enough (28 is ample) that intermediate products never lose digits, and quantize exactly one figure — the per-claim spread — withROUND_HALF_UP. Never carry afloatthrough 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 tokenized402-D2reference plus the twopricing_versionstamps 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.gatheror 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
Decimalcontext, quantization order, banker’s-versus-half-up rounding choice, and unit normalization tied to442-E7Quantity Dispensed and409-D9Ingredient Cost that make the spread reproducible to the cent.
Related
- Rebate Calculation & Accrual Tracking — the accrual model and rebate lifecycle that consume every spread computed here.
- Manufacturer Rebate Contract Modeling — the contract formulas (
% of WAC, AMP-anchored) that turn a spread into a rebate obligation. - Rebate Accrual Reconciliation — reconciles accrued spread-derived value against invoiced rebates and flags negative-spread lines.
- Tier Mapping & Copay Calculation Logic — the member-facing cost math that shares the
Decimalmoney discipline used here. - NDC-to-GPI Crosswalk Automation — resolves the
407-D7NDC to the GPI that benchmark and rebate attribution key on.
← Back to Rebate Calculation & Accrual Tracking