ArvexiBuilders Blog

The Math Behind a $52M Lease Liability

A Fortune 500 retailer signs a 30-year ground lease. Monthly rent starts at $145,000, escalates 3% annually, with 6 months of free rent and a $2.1M tenant improvement allowance. The discount rate is 5.5%.

The initial lease liability is $52,304,118.47. Not $52,304,118.48. Not $52,304,118.46. Exactly $52,304,118.47.

That precision is not academic. Under ASC 842, a $0.01 rounding error compounded over 360 months produces a $3.60 discrepancy. An auditor flags it. A restatement follows. For the 20 largest US public companies, total lease obligations exceed $300 billion. The math has to be right.

We built Arvexi's amortization engine as 1,875 lines of pure TypeScript: no database calls, no side effects. Every formula maps to a specific paragraph in ASC 842 or IFRS 16.

Architecture
The Calculation Pipeline
Decimal.js — 20-digit precision, ROUND_HALF_UP — underlies every stage
Click any stage for details · 1,875 lines of pure calculation code · zero side effects

The floating-point problem

JavaScript uses IEEE 754 double-precision. 0.1 + 0.2 === 0.30000000000000004. For weather data, irrelevant. For SEC filings, catastrophic.

A monthly rate of 0.055 / 12 is an infinitely repeating decimal that IEEE 754 truncates at the 17th significant digit. Multiply that truncated rate by a $52M balance, round incorrectly, and errors accumulate geometrically over a 30-year term.

We solved this with decimal.js, configured for 20-digit precision with banker's rounding:

// lib/utils/decimal.ts
Decimal.set({ precision: 20, rounding: Decimal.ROUND_HALF_UP });
 
export function round2(value: Decimal): Decimal {
  return value.toDecimalPlaces(2, Decimal.ROUND_HALF_UP);
}
 
export const ZERO = Object.freeze(new Decimal(0));

Every monetary value is a Decimal, never a JavaScript number. round2() is called at every intermediate step, not just at the end. ASC 842 requires each period's amounts be individually rounded. The distinction matters.

Interactive
Floating-Point Error Accumulation
IEEE 754 vs per-step rounding over 120 months
// JavaScript floating-point arithmetic
0.1 + 0.2 === 0.30000000000000004
// Monthly interest rate: 5.5% / 12
IEEE 754: 0.0045833333333333334
Decimal.js: 0.00458333333333333333
Compound over:120 mo
+$0.02$0.00-$0.02Month 1Month 120
Cumulative error after 120 months
$-0.01
Within tolerance. Decimal.js prevents this entirely.
Drag the slider to see how errors compound over longer lease terms

Overlapping payment streams

Real leases are not "pay $X for Y years." Our ground lease has fixed rent, 3% annual escalation, 6 months of free rent, quarterly CAM payments, and a step schedule at year 11. Each is a separate PaymentStream.

The PV engine expands all streams into a unified monthly timeline, aggregates overlapping months, then discounts each back to commencement. Three design decisions:

  1. Escalation skips free rent. The base rate does not compound during the free period. Getting this wrong shifts a 20-year liability by ~$40,000.

  2. Non-monthly frequencies emit zeroes. A quarterly $12K payment becomes $12K in months 0, 3, 6, 9 and $0 elsewhere, keeping the timeline uniform for discounting.

  3. Step rent overrides escalation. When both exist, the step schedule wins, modeling multi-phase deals with explicit period rents.

The discount factor handles beginning vs end-of-period timing:

const n = timing === "beginning" ? period : period + 1;
const discountFactor = new Decimal(1).div(new Decimal(1).add(rate).pow(n));
const pv = round2(amount.mul(discountFactor));

This single toggle changes the initial liability by approximately $237,000 on a 30-year lease.


The effective interest method

With the present value computed, we unwind the liability month by month. One invariant must hold in every single row:

beginningBalance + interestExpense − leasePayment = endingBalance

This is not an approximation. It is an identity. The implementation handles two timing variants and mid-month proration. If a lease commences January 15th, the first period accrues (17/31) × monthlyRate instead of the full rate. Without this, a lease starting January 30th would accrue a full month of interest for 2 days.


The straight-line plug

This is the most counterintuitive concept in ASC 842.

For finance leases, the P&L is straightforward: interest (declining) + depreciation (constant) = total expense (front-loaded).

For operating leases, ASC 842-20-25-6 requires total expense be straight-line. But the liability still amortizes with effective interest. This creates a contradiction: interest is front-loaded, but total expense must be flat.

The resolution: depreciation is the residual needed to make total expense equal the straight-line amount.

depreciation = straightLineExpense − interestExpense

Early periods: high interest, low depreciation. Late periods: low interest, high depreciation. The sum is always flat.

Visualization
The Straight-Line Plug
Finance Lease
Total expense: front-loaded (declines over time)
$0$136K
Operating Lease
Total expense: flat (straight-line)
$0$136K
Interest
Depreciation
Total expense
Same liability. Same interest curve. The operating lease “plug” adjusts depreciation each period so that interest + depreciation = a constant. Early periods: high interest, low depreciation. Late periods: low interest, high depreciation.
Hover over the charts to see per-month breakdowns

This is why IFRS 16 eliminated the operating lease concept for lessees. Every lease uses the finance model. No plug. No counterintuitive depreciation. We generate both schedules for dual reporting, and the P&L pattern difference is often the first thing a CFO's team asks about.


Final-period settling

Each round2() call introduces up to $0.005 of error. Over 360 periods, cumulative drift can reach several dollars. The ending balance should be exactly $0.00, but without correction it lands at something like +$2.37.

The settling adjustment absorbs the entire drift in the final period. A 10% safety cap prevents silently masking upstream errors. If the adjustment exceeds 10% of a regular payment, something went wrong in the calculation, and we log a warning instead of hiding it.


The 5-test classification gate

Before computing the schedule, we need to know whether a lease is operating or finance. ASC 842-10-25-2 defines five tests. If any single test passes, the lease is classified as finance.

Interactive
The 5-Test Classification Gate
1
Transfer of Ownership
qualitativeAI judgment
FAIL
2
Bargain Purchase Option
qualitativeAI judgment
FAIL
3
Lease Term ≥ 75% Useful Life
quantitativeServer-verified· threshold: 75%
FAIL
4
PV ≥ 90% Fair Value
quantitativeServer-verified· threshold: 90%
FAIL
5
Specialized Asset
qualitativeAI judgment
FAIL
All tests fail →
Classification
OPERATING
Straight-line expense
0 of 5 tests passing
Toggle any test to see how classification changes · Click a test for details · ASC 842-10-25-2

Tests 1, 2, and 5 are qualitative; AI evaluates them from the lease document. Tests 3 and 4 are quantitative; AI evaluates, then the server recomputes with exact values and overrides.

A critical subtlety in test 4: the guaranteed residual value uses its full amount for classification but only the probable amount for measurement. This dual treatment is validated by Deloitte DART and PwC guidance. We have seen other platforms get this wrong, flipping classification results near the 90% threshold.


Validation: four invariants, every row, in production

After generating a schedule, four invariant checks run on every row, not in CI, in production, on every generation:

RuleCheckTolerance
Balance equationbeginning + interest − payment = ending$0.02
Period continuityending[N] = beginning[N+1]$0.02
Non-negativeending ≥ 0$0.02
Terminal zeroending[final] = $0.00$1.00

The $0.02 tolerance accommodates per-step rounding. $0.01 would produce false positives on long-term leases. $0.02 gives one penny of headroom while catching any real calculation error.


By the numbers

FileLinesPurpose
present-value.ts176Payment expansion + PV computation
amortization.ts157Effective interest method
depreciation.ts107Straight-line and the operating plug
schedule-generator.ts438Orchestrator
validation.ts1244-invariant verification
ifrs16-schedule.ts236IFRS 16 dual reporting
fx-translation.ts205ASC 830 currency translation
decimal.ts29Decimal.js configuration

1,875 lines of pure calculation code. Zero database imports. Every function is a pure transformation: Decimal in, Decimal out. When an auditor asks "how did you compute this number," the answer is a pure function with named inputs that maps directly to a paragraph in the accounting standard.


See Lease Accounting to learn more about these capabilities.

Future posts cover the AI extraction pipeline (how we get the inputs), the modification engine (what happens when terms change mid-stream), and the journal entry generator (how schedule rows become GAAP-compliant entries).