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.
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 be the tenant’s pro-rata share of the current-year recoverable pool, and let be the stored threshold. The recoverable amount is the band above the stop, floored at zero:
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 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 by tenant rentable area over building rentable area :
For a full-net lease, and the tenant owes their entire share. For a fixed expense stop quoted per rentable square foot at rate , the stored stop is simply the rate times the tenant’s own area:
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 that does not vary with occupancy and a variable component that does, then scales the variable part from the actual base-year occupancy up to the target occupancy :
The grossed base-year pool then feeds the same pro-rata basis to produce the stored base-year stop, . 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
stopat 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_yearand 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_YEARprovision unlessbase_year_grossedis 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_validatorrejects 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.
Related
- Calculating base-year expense stops in Python — the step-by-step implementation of the stop and clamp math this page models.
- Handling base-year gross-up in CAM recoveries — why and how the base-year pool is normalized to a target occupancy before it becomes a floor.
- Building a lease abstraction database — where the recovery type, base-year amount, and gross-up target this stage reads are extracted and versioned.
- Gross-up normalization logic — the portfolio-wide engine that scales variable expenses to occupancy and produces the grossed base-year total.
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.