Applying a Negotiated Indirect Cost Rate Agreement in Python

Apply a NICRA correctly in Python: model rate segments with effective periods and base definitions, select the rate by each cost's incurred date, apply it to MTDC, and true up provisional to final under 2 CFR 200.414.

This guide is part of the Indirect Cost Rate Automation section, and it solves one narrow problem: when a cognizant agency has fixed your indirect cost rate, base, and effective periods in a signed agreement, how do you apply exactly the right rate to each cost — even when a multi-year award crosses two or three rate periods — and reject any cost that falls outside every covered segment?

A Negotiated Indirect Cost Rate Agreement (NICRA) is not a single number. It is a schedule of rate segments, each with an effective start and end date, a rate type (provisional, predetermined, fixed, or final), and a named base definition against which the rate is charged. The common mistake is to store one float — 0.34 — and multiply every direct cost by it. That over- or under-recovers indirect the moment an award spans a rate boundary, applies the rate to the wrong base, and leaves nothing to reconcile when a provisional rate is later finalized. This guide models the agreement faithfully, selects the segment by the cost’s incurred date, applies the rate to the Modified Total Direct Cost base, and handles the provisional-to-final true-up.

Selecting a NICRA rate segment by a cost's incurred date and applying it to the MTDC base A NICRA is drawn as a timeline of three consecutive rate segments: a predetermined 34 percent segment for fiscal year 2024, a provisional 36 percent segment for fiscal year 2025, and a fixed 35 percent segment for fiscal year 2026, each covering a date range. An incurred cost dated in the fiscal year 2025 window drops onto the timeline and selects the overlapping provisional segment. The selected rate flows right into an MTDC base computation, which multiplies the rate by the modified total direct cost base to produce indirect recovery. A separate cost dated outside every segment falls to a rejection branch labelled out of coverage, raising a structured error rather than defaulting to any rate. One agreement, many segments — the incurred date picks the rate Each cost selects exactly one covered segment, or it is rejected — never defaulted to a neighbouring rate. predetermined 34% 2023-10-01 → 2024-09-30 provisional 36% 2024-10-01 → 2025-09-30 fixed 35% 2025-10-01 → 2026-09-30 cost incurred 2025-03-14 · $12,000 MTDC select segment rate = 36% MTDC base §200.1 exclusions recovery $4,320 cost 2026-11-02 after last segment rejected out of coverage no segment covers this date

When to Use This Approach

Reach for this segment-aware applier when all three conditions hold:

  • The organization has a signed NICRA, not the de minimis rate. A NICRA is negotiated with, and issued by, the entity’s cognizant agency for indirect costs under 2 CFR §200.414. If instead the entity is electing the flat 10% rate available to those without a negotiated agreement, that is a different calculation with no rate segments and no true-up — it lives in Automating the 10% De Minimis Indirect Cost Rate.
  • The award spans more than one rate period. A single-year charge against a single provisional rate does not need this machinery. The moment a multi-year award has costs that straddle a rate boundary — say a September and an October charge on either side of a fiscal-year turn — a scalar rate is wrong and date-based segment selection is mandatory.
  • The base is defined, and it excludes items. The agreement names a base, most often Modified Total Direct Cost, which excludes equipment, capital expenditures, rent, participant support, and the portion of each subaward beyond the first $50,000. You must apply the negotiated rate to that computed base, not to raw total direct cost.

Computing the MTDC base itself — the exclusion rules and the subaward cap — is treated here as a reusable dependency rather than re-derived; this page focuses on segment modelling and rate selection. Rate negotiation, cost-pool allocation, and the assembly of the final indirect cost proposal are out of scope; how grantor-specific rate rules are encoded belongs to Grantor-Specific Rule Taxonomies.

Step-by-Step Implementation

The reference implementation targets Python 3.10+ and uses only the standard library — dataclasses, datetime, decimal, enum, and logging. Money is Decimal throughout; a rate applied to a budget in floating point drifts by cents that an auditor will find.

text
python >= 3.10   # standard library only — no third-party packages

Step 1: Model the agreement as dated rate segments

Represent each row of the NICRA as an immutable RateSegment: the effective window, the rate type, the rate as a Decimal, and the name of the base it charges against. A RateType enum makes the provisional-versus-final distinction explicit, because only provisional rates are subject to a later true-up.

python
import logging
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from enum import Enum

logger = logging.getLogger("nicra.apply")


class RateType(Enum):
    PROVISIONAL = "provisional"
    PREDETERMINED = "predetermined"
    FIXED = "fixed"
    FINAL = "final"


@dataclass(frozen=True)
class RateSegment:
    """One row of a Negotiated Indirect Cost Rate Agreement."""
    effective_start: date
    effective_end: date  # inclusive last covered day
    rate: Decimal
    rate_type: RateType
    base: str  # e.g. "MTDC" — the applicable base definition

    def covers(self, incurred: date) -> bool:
        return self.effective_start <= incurred <= self.effective_end

The effective_end is treated as an inclusive last covered day, so a cost incurred on the segment’s final date still matches. Freezing the dataclass guarantees a segment cannot be mutated after the agreement is loaded — the schedule is a fact, not a working value.

Step 2: Select the correct segment by the cost’s incurred date

Selection is the heart of the applier. Scan the agreement for the single segment whose window contains the cost’s incurred date. Zero matches is an out-of-coverage rejection, not a fallback to the nearest rate; more than one match means the agreement itself has overlapping periods and is malformed, which must fail loudly at load time.

python
class CoverageError(Exception):
    """Raised when no rate segment covers a cost's incurred date."""


class AgreementError(Exception):
    """Raised when the agreement's segments overlap or are inconsistent."""


def select_segment(segments: list[RateSegment], incurred: date) -> RateSegment:
    matches = [seg for seg in segments if seg.covers(incurred)]
    if len(matches) > 1:
        raise AgreementError(f"Overlapping segments cover {incurred.isoformat()}")
    if not matches:
        logger.warning("no_segment_covers date=%s", incurred.isoformat())
        raise CoverageError(f"No negotiated rate covers {incurred.isoformat()}")
    return matches[0]

Returning the matched segment rather than its bare rate keeps the base name and rate type attached, so the caller applies the right rate to the right base and knows whether a true-up will follow.

Step 3: Apply the segment’s rate to its specified base

Multiply the negotiated rate by the base amount the agreement names — here the pre-computed MTDC figure. The MTDC base itself, with its §200.1 exclusions and the $50,000 subaward cap, is produced by a reusable base computation; this function consumes that result and quantizes the product to cents.

python
from decimal import ROUND_HALF_UP

CENTS = Decimal("0.01")


def apply_rate(segment: RateSegment, mtdc_base: Decimal, incurred: date) -> Decimal:
    if segment.base != "MTDC":
        raise AgreementError(f"Unsupported base {segment.base!r} on segment")
    if mtdc_base < 0:
        raise ValueError(f"MTDC base cannot be negative: {mtdc_base}")
    recovery = (segment.rate * mtdc_base).quantize(CENTS, rounding=ROUND_HALF_UP)
    logger.info(
        "indirect_applied date=%s type=%s rate=%s base=%s recovery=%s",
        incurred.isoformat(), segment.rate_type.value, segment.rate, mtdc_base, recovery,
    )
    return recovery

Guarding on segment.base means a NICRA that names a base this code does not implement (salaries-and-wages, for instance) fails explicitly rather than silently applying the rate to an MTDC figure it was never meant for.

Step 4: True up a provisional rate to its final rate

A provisional rate is a billing estimate. When the cognizant agency later issues the final rate for that period, every provisional recovery must be recomputed at the final rate and the difference settled. Compute the delta per cost; a positive delta is additional recovery owed to the grantee, a negative delta is an amount to return.

python
@dataclass(frozen=True)
class TrueUp:
    incurred: date
    provisional_recovery: Decimal
    final_recovery: Decimal
    delta: Decimal  # final minus provisional; positive = owed to grantee


def true_up(segment: RateSegment, final_rate: Decimal,
            mtdc_base: Decimal, incurred: date) -> TrueUp:
    if segment.rate_type is not RateType.PROVISIONAL:
        raise AgreementError("Only provisional segments are subject to true-up")
    provisional = (segment.rate * mtdc_base).quantize(CENTS, rounding=ROUND_HALF_UP)
    final = (final_rate * mtdc_base).quantize(CENTS, rounding=ROUND_HALF_UP)
    delta = final - provisional
    logger.info("true_up date=%s delta=%s", incurred.isoformat(), delta)
    return TrueUp(incurred, provisional, final, delta)

Only PROVISIONAL segments are eligible: predetermined, fixed, and final rates are not retroactively adjusted, so calling true_up on them raises rather than fabricating a phantom settlement.

Step 5: Reject costs that fall outside every segment

Wire the pieces into one entry point. A cost whose date no segment covers propagates the CoverageError — it is never charged at a default rate. This is the compliance-critical behaviour: unallowable indirect recovery on an uncovered date is a finding under 2 CFR §200.414.

python
def recover_indirect(segments: list[RateSegment],
                     incurred: date, mtdc_base: Decimal) -> Decimal:
    segment = select_segment(segments, incurred)  # raises CoverageError if uncovered
    return apply_rate(segment, mtdc_base, incurred)

Because select_segment raises before apply_rate is reached, there is no code path that returns a recovery amount for an out-of-coverage cost — the rejection is structural, not a runtime check the caller might forget.

Verification

Confirm the applier behaves correctly with four checks:

  1. The incurred date selects the right segment. With three consecutive segments, a cost dated inside the middle window recovers at the middle rate, and a cost dated one day into the next window recovers at the next rate — proving the boundary is handled inclusively and selection is date-driven, not order-driven.
  2. An uncovered date is rejected. A cost dated after the last segment’s effective_end raises CoverageError and produces no recovery figure, satisfying the rule that no default rate is ever applied.
  3. The true-up settles the delta. A provisional recovery re-run at a lower final rate yields a negative delta equal to (final_rate - provisional_rate) * mtdc_base, quantized to cents.
  4. A malformed agreement fails loudly. Two segments whose windows overlap raise AgreementError on selection rather than silently returning the first match.
python
segments = [
    RateSegment(date(2023, 10, 1), date(2024, 9, 30), Decimal("0.34"),
                RateType.PREDETERMINED, "MTDC"),
    RateSegment(date(2024, 10, 1), date(2025, 9, 30), Decimal("0.36"),
                RateType.PROVISIONAL, "MTDC"),
    RateSegment(date(2025, 10, 1), date(2026, 9, 30), Decimal("0.35"),
                RateType.FIXED, "MTDC"),
]
assert recover_indirect(segments, date(2025, 3, 14), Decimal("12000")) == Decimal("4320.00")
try:
    recover_indirect(segments, date(2026, 11, 2), Decimal("5000"))
    raise AssertionError("expected CoverageError")
except CoverageError:
    pass

Common Errors & Fixes

Error Cause Fix
Indirect over- or under-recovers on a multi-year award One scalar rate applied across a rate boundary Model each period as a RateSegment and call select_segment on the cost’s incurred date.
CoverageError on a valid-looking cost Incurred date falls in a gap between segments, or before the first effective_start Confirm the agreement has no gaps for the award period; if the date is truly uncovered, the cost is unallowable indirect — do not default a rate.
Recovery is a few cents off the funder’s figure Rate applied to a float budget, or rounding not fixed Use Decimal end to end and quantize(Decimal("0.01"), ROUND_HALF_UP).
AgreementError: Overlapping segments Two rows in the NICRA share a covered day Correct the boundary so one segment’s effective_end is the day before the next effective_start.
True-up produces a settlement on a fixed rate true_up called on a non-provisional segment Gate on rate_type is RateType.PROVISIONAL; predetermined, fixed, and final rates are not adjusted.