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.
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.
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:
-
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.
-
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.
-
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.
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.
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:
| Rule | Check | Tolerance |
|---|---|---|
| Balance equation | beginning + interest − payment = ending | $0.02 |
| Period continuity | ending[N] = beginning[N+1] | $0.02 |
| Non-negative | ending ≥ 0 | $0.02 |
| Terminal zero | ending[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
| File | Lines | Purpose |
|---|---|---|
present-value.ts | 176 | Payment expansion + PV computation |
amortization.ts | 157 | Effective interest method |
depreciation.ts | 107 | Straight-line and the operating plug |
schedule-generator.ts | 438 | Orchestrator |
validation.ts | 124 | 4-invariant verification |
ifrs16-schedule.ts | 236 | IFRS 16 dual reporting |
fx-translation.ts | 205 | ASC 830 currency translation |
decimal.ts | 29 | Decimal.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).