Build guide · About 35 minutes · Rated High on the map

Check that your property manager billed the rate in the contract

The management fee is usually the second largest controllable line on the statement and the one nobody recomputes. It arrives as a single number with a percentage next to it, the percentage matches the contract, and everyone moves on. The percentage is rarely the problem. What it was applied to is the problem.

Why this drifts without anybody being dishonest

A management agreement says something like four percent of collected revenue. Then the bookkeeping has to decide what collected revenue means, and the contract usually does not say. Does it include late fees. Does it include the application fees the manager keeps anyway. Does it include a $40,000 insurance settlement that landed in the operating account in March. Does it include prepaid rent collected in December for January.

Each of those is a defensible answer and each moves the fee. A property doing $250,000 a month in rent with $18,000 of other income pays $720 a month more if other income is in the base than if it is out. Nobody decided that. It came out of how the software was configured on day one, and it has been compounding ever since.

Recomputing takes four lines of arithmetic. The reason it never happens is that nobody owns the question, not that it is hard.

What you need

The build

  1. Write the base down before you write any code This is the whole build and it takes an hour of reading. Produce one sentence per property that says exactly which lines are in and which are out, and get the manager to confirm it in an email. That email is worth more than the automation, because a disagreement discovered in writing before it compounds is a conversation, and one discovered eighteen months later is a dispute.
    // one config per property, agreed in writing
    const OAKWOOD_FEE = {
      rate: 0.04,
      base: ["rent_collected", "other_income_pet", "other_income_parking"],
      exclude: ["application_fees", "late_fees", "insurance_proceeds",
                "utility_reimbursement", "security_deposit"],
      basis: "collected",     // "collected" or "billed"
      minimumMonthly: 2500
    };
  2. Recompute from the statement
    function expectedFee(statement, cfg) {
      const base = cfg.base.reduce((sum, line) => sum + (statement[line] || 0), 0);
      const raw  = base * cfg.rate;
      return Math.max(raw, cfg.minimumMonthly || 0);
    }
    
    const expected = expectedFee(march, OAKWOOD_FEE);
    const charged  = march.management_fee;
    const variance = charged - expected;
    const pct      = variance / expected;
  3. Set a floor on both dollars and percent Rounding, timing and a cent here or there will otherwise generate an alert every month and the report will stop being read. Require the variance to clear both tests before anybody hears about it.
    const flag = Math.abs(variance) > 75 && Math.abs(pct) > 0.01;
  4. Track it as a series, not as a month A single month over by $90 is noise. The same $90 for nine months is $810 and a configuration error that will keep running. Keep the monthly variance in a table and look at the sign. Variances that alternate are timing. Variances with a consistent sign are a rule.
  5. Run the same check on the other manager-billed lines Once the base is written down, the same arithmetic covers leasing commissions, renewal fees, construction management fees on capital work, and any markup on maintenance labour. Those are smaller individually and they drift in the same way, for the same reason.

What the variance usually turns out to be

PatternUsual cause
Fee consistently high by a steady percentageOther income is in the base and your reading of the contract says it should not be. The most common finding and the largest.
Fee high in one month, low the next, netting near zeroTiming. Rent collected across a month boundary, or a fee charged on billed revenue while you are computing on collected. Not worth chasing beyond confirming the basis.
Fee high in a month with a large non-operating receiptAn insurance settlement, a tax refund or a deposit release landed in revenue and got a percentage taken out of it.
Fee flat regardless of revenueThe minimum monthly is binding. Correct, but worth knowing, because it means the manager is indifferent to collections at this property.
Fee lowUsually a partial month or a concession the manager granted and did not mention. Worth the same email as an overcharge.

Three things that will break it

The chart of accounts changes underneath you. The manager adds a revenue line, or splits one in two, and your base config silently stops including something it used to. Alert when the statement contains a revenue account you have never mapped, rather than quietly summing the ones you recognize. A base that shrinks without anybody noticing produces an expected fee that is too low and an alert that blames the manager for something you broke.

Billed against collected. If the contract says collected and the software charges on billed, the fee runs high in any month with delinquency and corrects when the arrears come in. That reads as random noise until you compare it to the delinquency movement, at which point it becomes obvious. Check the basis first, in writing, before you conclude anything from the variance.

Prepaid rent. December collections for January revenue will either double-count or disappear depending on whether the statement is cash or accrual and whether the fee follows the same basis. This is the one that generates a large single-month variance that looks alarming and resolves itself the following month. Keep the series, look at the pair.

If you would rather not build it

The kit is the finished version of everything above: the scenario blueprint, the fee configuration per property, the recompute and the variance series, and the thresholds as a spreadsheet you can edit. Email [email protected] if you would rather have it built into what you already run.

Get the next build guide

One email when a new guide goes up. Nothing else.

No sequence, no upsell drip. Unsubscribe link on every send.