Modeling Expense Stops and Base-Year Clauses

An expense stop caps how much operating cost a landlord absorbs before a tenant begins reimbursing, and modeling it correctly means storing the negotiated threshold as structured lease data the allocation engine can subtract on its own. This is the recovery-structure layer of the CAM architecture and lease clause taxonomy, and it is where three different lease economics — full-net pass-through, a fixed expense stop, and a base-year floor — have to be reduced to one uniform stored rule so that every downstream calculation reads them the same way. Get the model wrong and the failure is quiet and expensive: a tenant billed on the full pool when their lease only owes the increase over a base year, or a base year left un-grossed so the tenant reimburses inflation that never happened. For real estate accountants, lease administrators, and the Python teams automating recovery, expense-stop modeling is the point where negotiated legal economics become a deterministic subtraction the engine performs without rereading the lease.

Expense-stop recovery as the band above the stop A dollars-per-rentable-square-foot axis rises on the left. A dashed base-year stop line runs horizontally across both bars. The left bar represents a year whose current-year share exceeds the stop: the portion above the stop is the shaded recoverable band, the portion below is covered by the base year. The right bar represents a year whose current-year share falls below the stop, so nothing is recoverable. The recoverable amount is the maximum of zero and current share minus stop. $/RSF Base-year stop Current share > stop Recoverable Covered by base year Current share ≤ stop No recovery recoverable = max(0, current share − stop)
Recovery under a stop is only the band of a tenant's current-year share that rises above the negotiated threshold; when the share stays under the stop, recovery is zero.

Three recovery structures dominate commercial leases, and the whole point of this stage is to store all of them behind one field the engine can branch on. A full-net (NNN) lease recovers the tenant’s entire pro-rata share with no threshold at all. An expense-stop lease recovers only the share above a fixed floor. A base-year lease is the same subtraction, except the floor is derived from what the landlord actually spent in a negotiated base year rather than stated as a round number. Modeled well, the difference between them collapses to a single stored stop amount and a recovery_type tag, and every year-end true-up becomes the same clamp.

Prerequisites & Data Contracts

Expense-stop modeling cannot begin until the lease itself is structured and its expense pool is already classified, because a stop is meaningless without a stable pool to subtract it from. Two upstream contracts have to be satisfied first.

The lease terms must already be abstracted. The base year, the base-year amount or the stop expressed per rentable square foot, the recovery type, the gross-up target occupancy, and any linkage to a controllable-expense cap are all attributes of the executed lease, not decisions made at reconciliation time — so they have to be extracted and persisted in the lease abstraction database before any recovery runs. The minimum abstracted contract for this stage is a recovery_type enum, exactly one of base_year_amount or stop_per_rsf, the base_year label, tenant and building rentable square footage, a gross_up_target occupancy fraction, and an optional reference to the controllable cap that governs the same pool. A provision that arrives with both a base-year amount and a per-RSF stop, or with neither, is malformed and must be blocked rather than guessed at.

The expense pool must be built from consistent categories. A stop subtracts against a total, and that total is only trustworthy when every line feeding it was classified the same way — so the recoverable pool must already have been assembled using the same rules described in defining CAM expense categories in your lease. If one lease folds management fees into the operating pool and another excludes them, then a base-year amount abstracted from the first lease is not comparable to a current-year pool built from the second, and the subtraction produces a number no auditor will accept. The stop and the pool it is measured against must share a category definition, or the model is comparing two different quantities.

The output contract this stage promises downstream is narrow and strict: every provision resolves to a stored recovery_type, a single canonical stop amount in dollars (already grossed up and already converted from per-RSF to a lump sum where needed), and enough provenance to reproduce how that stop was derived. Downstream allocation never re-parses the lease; it reads the stored stop and clamps.

Algorithm or Rule Design

Every recovery structure reduces to one clamp. Let current_sharecurrent\_share be the tenant’s pro-rata share of the current-year recoverable pool, and let stopstop be the stored threshold. The recoverable amount is the band above the stop, floored at zero:

recoverable=max ⁣(0,  current_sharestop)recoverable = \max\!\left(0,\; current\_share - stop\right)

The clamp to zero is not cosmetic. In a soft market operating costs can fall, and a tenant’s current-year share can land below the stop; the lease does not turn that shortfall into a landlord refund, so the model floors it at zero rather than emitting a negative recovery. The three recovery types differ only in how stopstop is computed, never in this final subtraction.

The tenant’s current-year share follows the standard rentable-area basis, distributing the current-year recoverable pool EcurE_{cur} by tenant rentable area AtA_t over building rentable area AbA_b:

current_share=AtAb×Ecurcurrent\_share = \frac{A_t}{A_b} \times E_{cur}

For a full-net lease, stop=0stop = 0 and the tenant owes their entire share. For a fixed expense stop quoted per rentable square foot at rate srsfs_{rsf}, the stored stop is simply the rate times the tenant’s own area:

stop=srsf×Atstop = s_{rsf} \times A_t

For a base-year lease, the stop is the tenant’s pro-rata share of what the landlord spent in the base year — but of the base-year pool after gross-up, not the raw figure. This is the subtlety that separates a defensible base-year model from a naive one. A base year measured during a partially vacant building understates the variable operating costs the building incurs when full. If the base year is left un-grossed, the tenant’s floor is artificially low, and as the building leases up the tenant reimburses the entire cost of filling formerly empty space — increases they never caused. Grossing the base year up to a stated occupancy corrects this: variable expenses are scaled to what they would have been at the target occupancy, so the floor reflects a normalized full-building cost and the tenant only ever pays for genuine cost growth.

Conceptually, gross-up splits the base-year pool into a fixed component EfixedE_{fixed} that does not vary with occupancy and a variable component EvarE_{var} that does, then scales the variable part from the actual base-year occupancy occactocc_{act} up to the target occupancy occtgtocc_{tgt}:

Ebasegross=Efixed+Evar×occtgtoccactE_{base}^{gross} = E_{fixed} + E_{var} \times \frac{occ_{tgt}}{occ_{act}}

The grossed base-year pool then feeds the same pro-rata basis to produce the stored base-year stop, stop=(At/Ab)×Ebasegrossstop = (A_t / A_b) \times E_{base}^{gross}. The mechanics of that scaling — which categories count as variable, how caps and occupancy floors interact — are their own discipline and live in gross-up normalization logic; this stage’s responsibility is only to store the resulting stop and to record that it was grossed. Decision logic across the three types is therefore a single branch: NNN sets the stop to zero, EXPENSE_STOP multiplies a per-RSF rate by tenant area, and BASE_YEAR takes the pro-rata share of the grossed base-year pool. After that branch, all three converge on the identical clamp.

Python Implementation

The implementation models the provision as a typed object and exposes a pure function that turns a stored stop and a current-year share into a recoverable amount. Every monetary value uses the decimal module rather than binary floating point, because a base-year subtraction magnifies rounding error — two large, nearly equal numbers differenced together will surface any sub-cent drift as a visible, disputable variance on the tenant statement. Areas and occupancy ratios are Decimal as well.

from __future__ import annotations

from decimal import Decimal, ROUND_HALF_UP
from enum import Enum

from pydantic import BaseModel, Field, model_validator

CENTS = Decimal("0.01")


class RecoveryType(str, Enum):
    """How a lease structures operating-expense recovery."""

    NNN = "nnn"                 # full pro-rata share, no threshold
    EXPENSE_STOP = "expense_stop"  # share above a fixed per-RSF floor
    BASE_YEAR = "base_year"     # share above a grossed base-year floor


class StopProvision(BaseModel):
    """A versioned expense-stop / base-year provision for one lease.

    Exactly one of `base_year_amount` or `stop_per_rsf` may be set, and
    only when the recovery_type calls for it. The provision is immutable
    once stored; a mid-term reset creates a new version rather than a
    mutation, preserving the audit trail.
    """

    lease_id: str
    version: int = Field(ge=1)
    recovery_type: RecoveryType
    tenant_rsf: Decimal = Field(gt=0)
    building_rsf: Decimal = Field(gt=0)
    # base-year lease: the grossed-up base-year pool total (dollars)
    base_year_amount: Decimal | None = None
    # expense-stop lease: the negotiated floor per rentable square foot
    stop_per_rsf: Decimal | None = None
    base_year_grossed: bool = False

    @model_validator(mode="after")
    def _one_stop_source(self) -> "StopProvision":
        if self.recovery_type is RecoveryType.NNN:
            if self.base_year_amount is not None or self.stop_per_rsf is not None:
                raise ValueError("NNN provision must carry no stop figure")
        elif self.recovery_type is RecoveryType.EXPENSE_STOP:
            if self.stop_per_rsf is None or self.base_year_amount is not None:
                raise ValueError("expense_stop requires stop_per_rsf only")
        elif self.recovery_type is RecoveryType.BASE_YEAR:
            if self.base_year_amount is None or self.stop_per_rsf is not None:
                raise ValueError("base_year requires base_year_amount only")
            if not self.base_year_grossed:
                # a base year fed to recovery ungrossed is a latent overcharge
                raise ValueError("base_year_amount must be grossed up first")
        return self

    @property
    def pro_rata_share(self) -> Decimal:
        """A_t / A_b under the lease's rentable-area basis."""
        return (self.tenant_rsf / self.building_rsf).quantize(
            Decimal("0.00000001"), rounding=ROUND_HALF_UP
        )

    def stop_amount(self) -> Decimal:
        """The canonical stop in dollars, resolved from the provision.

        NNN -> 0; EXPENSE_STOP -> rate * tenant area; BASE_YEAR ->
        tenant pro-rata share of the grossed base-year pool.
        """
        if self.recovery_type is RecoveryType.NNN:
            stop = Decimal("0")
        elif self.recovery_type is RecoveryType.EXPENSE_STOP:
            assert self.stop_per_rsf is not None
            stop = self.stop_per_rsf * self.tenant_rsf
        else:  # BASE_YEAR
            assert self.base_year_amount is not None
            stop = self.pro_rata_share * self.base_year_amount
        return stop.quantize(CENTS, rounding=ROUND_HALF_UP)

The provision object stores the stop; a second, deliberately tiny function performs the recovery clamp so the arithmetic that reaches the tenant statement is trivially auditable.

def compute_recoverable(current_share: Decimal, stop: Decimal) -> Decimal:
    """Recoverable = max(0, current_share - stop), quantized to cents.

    `current_share` is the tenant's pro-rata share of the current-year
    recoverable pool; `stop` is StopProvision.stop_amount(). A share
    that falls below the stop yields zero, never a negative recovery.
    """
    recoverable = current_share - stop
    if recoverable < Decimal("0"):
        recoverable = Decimal("0")
    return recoverable.quantize(CENTS, rounding=ROUND_HALF_UP)


def current_year_share(pool_total: Decimal, provision: StopProvision) -> Decimal:
    """Tenant's pro-rata share of the current-year recoverable pool."""
    share = provision.pro_rata_share * pool_total
    return share.quantize(CENTS, rounding=ROUND_HALF_UP)

Because stop_amount() collapses all three recovery types to one dollar figure, compute_recoverable never needs to know which structure produced it — the branch happens once, at storage time, and every downstream true-up is the same subtraction and clamp.

Validation Rules & Edge Cases

The stop model is where recovery quietly goes wrong, so it carries dedicated validation. Each failure mode below is specific to expense-stop and base-year provisions and implies a distinct remediation rather than a silent default.

  • Negative recovery. A soft-market year drops the tenant’s current-year share below the stop. The clamp to zero is the correct behavior — the lease does not convert a shortfall into a refund — but the raw negative should still be logged, because a persistently negative delta may signal that the base year was set too high or grossed against the wrong occupancy.
  • Per-RSF versus lump sum. A stop quoted as $1.85 per rentable square foot and a stop quoted as a $92,500 building total are different data shapes that must never be stored interchangeably. The model normalizes both into one canonical dollar stop at storage time and records which form it came from; mixing the two — multiplying a lump sum by area, or treating a per-RSF rate as a total — is a classic order-of-magnitude error.
  • Mid-term base-year reset. A renewal or expansion frequently resets the base year mid-lease. This is never an in-place edit of the stored amount; it creates a new provision version with a new base_year and effective date, so any historical reconciliation still resolves against the stop that was in force for that period. Versioning here mirrors the discipline in versioning lease amendments in a lease database.
  • Base year fed ungrossed. The most consequential edge case: a base-year amount that was captured during partial occupancy and never grossed up. The model refuses to accept a BASE_YEAR provision unless base_year_grossed is true, forcing the normalization to run first. The gross-up itself — and the fairness argument for it — is detailed in handling base-year gross-up in CAM recoveries and governed by the portfolio-wide gross-up normalization logic.
  • Both or neither stop source. A provision carrying both a base-year amount and a per-RSF rate, or a stop-type lease carrying neither, is structurally ambiguous. The model_validator rejects it at the schema boundary so a malformed provision never reaches the recovery math.

Because Pydantic enforces the positive-area constraints and the one-stop-source rule at construction, structurally malformed provisions never reach compute_recoverable. What the recovery function guards is the semantic question the lease actually poses: how much of this year’s cost growth the tenant owes above their negotiated floor.

Integration Points

The stored provision sits between the lease abstraction and the allocation engine, and its output is a contract several systems read directly.

The allocation engine consumes the canonical stop and the current-year share to produce the billed recovery. The share itself comes from the pro-rata allocation algorithm, which distributes the current-year recoverable pool by rentable area; the stop model then subtracts the stored floor from that share. Keeping the two concerns separate — allocation produces the share, the stop model produces the floor — means a change to one never silently corrupts the other, and the same pro-rata share can feed both a stop subtraction and a straight NNN pass-through depending only on the provision’s recovery_type.

The stop also interacts with cap enforcement. When a controllable-expense cap governs the same pool, the order of operations matters: the pool is clamped to its contractual ceiling before the current-year share is computed, so the tenant never reimburses above the cap even when their share above the stop would otherwise exceed it. That interaction is the province of managing expense caps and controllable limits, and the provision’s optional cap linkage is what tells the cap engine that this recovery is subject to both a floor and a ceiling. Downstream of both, the recoverable amount posts to the tenant statement with its full provenance — recovery type, stop derivation, and grossed base-year flag — so the year-end true-up is reproducible line by line.

Testing & Verification

Stop math is only trustworthy when its correctness is pinned by fixtures, and the arithmetic has a few properties that make good tests obvious. The recovery function is pure, so tests are table-driven: a stored stop and a current-year share in, an exact Decimal recoverable out.

from decimal import Decimal

import pytest

BASE_YEAR = StopProvision(
    lease_id="L-204",
    version=1,
    recovery_type=RecoveryType.BASE_YEAR,
    tenant_rsf=Decimal("18000"),
    building_rsf=Decimal("240000"),
    base_year_amount=Decimal("1920000.00"),  # grossed base-year pool
    base_year_grossed=True,
)


def test_base_year_stop_is_pro_rata_of_grossed_pool() -> None:
    # 18,000 / 240,000 = 0.075 exactly; 0.075 * 1,920,000 = 144,000.00
    assert BASE_YEAR.stop_amount() == Decimal("144000.00")


def test_recovery_is_band_above_stop() -> None:
    share = current_year_share(Decimal("2_040_000.00"), BASE_YEAR)
    # share = 0.075 * 2,040,000 = 153,000.00; recoverable = 153,000 - 144,000
    assert share == Decimal("153000.00")
    assert compute_recoverable(share, BASE_YEAR.stop_amount()) == Decimal("9000.00")


def test_share_at_the_stop_recovers_zero() -> None:
    # boundary: current-year share exactly equals the stop
    assert compute_recoverable(Decimal("144000.00"), Decimal("144000.00")) == Decimal("0.00")


def test_share_below_stop_clamps_to_zero() -> None:
    # soft-market year: share falls under the floor, no negative recovery
    assert compute_recoverable(Decimal("140000.00"), Decimal("144000.00")) == Decimal("0.00")


def test_ungrossed_base_year_is_rejected() -> None:
    with pytest.raises(ValueError):
        StopProvision(
            lease_id="L-205", version=1,
            recovery_type=RecoveryType.BASE_YEAR,
            tenant_rsf=Decimal("18000"), building_rsf=Decimal("240000"),
            base_year_amount=Decimal("1750000.00"),
            base_year_grossed=False,  # never grossed -> refused
        )

Three verification habits matter beyond the unit cases. First, always test the boundary at the stop itself — a share exactly equal to the floor must recover zero, and a share one cent above must recover one cent, because off-by-one clamp bugs hide precisely at the threshold. Second, assert against Decimal literals rather than float results; comparing a base-year difference to 9000.0 as a float can pass or fail on representation alone, defeating the reason for using decimal. Third, treat the computation as reproducible: the same provision version and the same current-year pool must always yield the identical recoverable, which is what lets a disputed true-up be recomputed and defended a year later.

Frequently Asked Questions

What is the difference between an expense stop and a base year? Both structures give the tenant a floor and recover only the amount above it, so mechanically they are the same subtraction. The difference is how the floor is set. An expense stop is a negotiated number — often a round dollar-per-rentable-square-foot rate — fixed in the lease regardless of actual spending. A base year derives the floor from what the landlord genuinely spent in a stated base year, so it tracks the building’s real cost profile. A base year is essentially an expense stop whose value is measured rather than negotiated, which is why the base year must be grossed up before it can serve as a fair floor.

Why must the base year be grossed up to a stated occupancy? Because a base year measured while the building is partly vacant understates the variable operating costs incurred at full occupancy. If the floor is left low, then as the building leases up the tenant reimburses the cost of filling formerly empty space — increases they never caused. Grossing the base year up to a target occupancy, typically 95 percent, scales the variable expenses to a normalized full-building level, so the tenant’s floor reflects a stabilized cost and they pay only for genuine growth over it, not for the building’s lease-up.

What happens when current-year expenses fall below the stop? Nothing is recoverable, and the model clamps the result to zero. The recovery formula is the maximum of zero and the current-year share minus the stop, so a share below the floor produces no charge. Crucially, the lease does not turn that shortfall into a landlord-funded refund to the tenant — the floor only ever works in one direction. A persistently negative delta is worth investigating, though, because it often means the base year was set too high or grossed against the wrong occupancy.

Can one lease have both a per-RSF stop and a base-year amount? No — a single recovery structure resolves to one floor. A per-RSF stop and a base-year amount are two different ways of expressing that floor, and a provision carrying both is ambiguous. The data model stores exactly one stop source per provision, normalizes it into a single canonical dollar figure, and rejects any provision that supplies both or neither. If a lease genuinely changes structure mid-term, that is modeled as a new provision version with its own effective date, not as two competing floors on one record.

The stop model earns its keep by making three different lease economics look identical to everything downstream. Once a full-net, expense-stop, or base-year provision has been reduced to a stored recovery_type and a single canonical stop, the pro-rata allocation algorithm produces the current-year share, managing expense caps and controllable limits applies any ceiling, and the recovery is the same clamp every time — turning negotiated lease language into a subtraction the engine can prove.