# CMAs

Source: https://help.ruuster.com/api/cma

> How Run a CMA chooses comparable sales, what each dollar adjustment is based on, how the value conclusion is reached, the daily allowance, and what a CMA from this API is not.

[Run a CMA](https://help.ruuster.com/api/cma-create) turns an address, MLS number or `propertyId` into a
comparative market analysis built from MLS records: the subject's profile, sold comps with dollar
adjustments, the active, pending and expired competition, market conditions, a value conclusion and a
seller net sheet. Everything is computed; nothing is written by an AI. Each analysis is stored and can be
read again for 90 days.

**Not an appraisal.** A CMA is a pricing tool. The adjustments are rule-based estimates with published
defaults, the comps are chosen by a formula, and the facts come from MLS records that can be incomplete or
stale. Check the subject's facts (and send `overrides` for anything you know better), look at the comps,
and use your judgment before putting a number in front of a seller.

## Where the subject can be

Anywhere your brokerage's MLSs cover. The brokerage is the limiter: a subject in an MLS you're not on
returns `404 not_found`. The subject doesn't need to be your listing, and it doesn't need to be on the
market: any address with an MLS record, including an old sale, works.

**Homes with no MLS history** (US only, in the states your MLSs cover). When an address has no MLS record, or you send
`subject.publicRecordId`, the home's county public record stands in: its building facts and location
become the subject (`subject.resolvedBy` is `publicRecord`, `subject.propertyId` is `null` and
`subject.recordId` names the record), and comps come from whichever of your MLSs has the most sales of
that kind of home within a mile over the past year. The county's facts can be out of date or missing (it
rarely knows about a remodel), so check them and send `overrides`; `subject.profile.sources` marks each
fact `publicRecord`, `override` or `mls`. Such a CMA has no photos and no listing link for the subject.

## How comps are chosen

1. **Same MLS, same property type** as the subject (house, townhouse or condo), not rentals.
2. **Within the radius** (default 0.75 miles), **sold within the window** (default 6 months), and **within
   the size band** (default ±20% of living area).
3. The same home listed in two overlapping MLSs counts once (the most recent sale wins).
4. Each sale is scored on distance, size difference, age difference, bedroom and bathroom difference and
   how long ago it sold; the best `maxComps` (default 6) are kept.
5. If fewer than `minComps` (default 3) distinct homes are found, the search widens to 1 mile, then 2
   miles, then 12 months, and `search.widened` says what happened.

## Adjustments and their defaults

Each comp is adjusted to what it would have sold for as the subject. Sale prices are first taken net of
seller concessions. Percentages are of the comps' median net sale price; every adjustment in the response
carries its `basis`.

| Factor | Default |
|---|---|
| Time | The market's monthly price-per-sq-ft movement × months since the sale, clipped to ±1% per month. No time adjustment in thin markets (fewer than 6 months with 3+ sales, or fewer than 30 sales in the year) |
| Living area | Each sq ft of difference at 50% of the area's median price per sq ft |
| Full bath / half bath | 2.5% / 1.25% per bath |
| Bedroom | 1.5%, at most one bedroom (living area already explains most of it) |
| Garage space | 1.5% per space |
| Lot size | 10% of the median price per acre per acre of difference, capped at ±5% |
| Pool | 3% |
| Basement | Finished 3%, unfinished 1% |
| View | 2% |
| Condition | 3% per step between poor, fair, average, good and excellent |
| Updates | Roof 1%, HVAC 1%, kitchen 3%, bathrooms 1.5%, flooring 1%, windows 1% |
| Age | 0.25% per year of difference, capped at ±5% |

Override any of them per request with `options.adjustmentRates` (`livingAreaShare`, `fullBathPct`,
`halfBathPct`, `bedroomPct`, `garageSpacePct`, `lotShare`, `lotCapPct`, `poolPct`, `basementFinishedPct`,
`basementUnfinishedPct`, `viewPct`, `conditionStepPct`, `agePerYearPct`, `ageCapPct`, `timeCapPctPerMonth`,
`grossFlagPct`, `updatePct.{roof,hvac,kitchen,bathrooms,flooring,windows}`). Facts that are unknown on
either side produce no adjustment. A comp whose gross adjustment is above 25% is flagged
`high_gross_adjustment` and weighs half as much.

## From comps to a value

- Each adjusted comp gets a weight that falls with its gross adjustment, its distance and its age, halved
  when flagged. `indicatedValue` is the weighted mean of the adjusted prices; `range` is the weighted
  25th to 75th percentile, never narrower than ±2%.
- `recommendedListPrice` is the indicated value divided by the area's median list-to-sale ratio, nudged
  1% up in a rising market or down in a declining one, rounded the way agents list.
- `confidence` is `high` with five or more similar, recent sales and small adjustments; `medium` or `low`
  as the comps get fewer, older, more spread out or more heavily adjusted. `reasons` says why.
- `market` describes the area within at least 1 mile over 12 months: monthly closings, medians, days on
  market, list-to-sale, absorption (actives now ÷ closings per month), a price trend and this month's share
  of the year's closings. `market.rates` carries the current 30- and 15-year fixed national averages
  with their dates (see [Mortgage rates](https://help.ruuster.com/api/mortgage-rates)); it is `null` only
  when the rate source is unreachable.
- `netSheet` is arithmetic on the list price you give (or the recommended one): commission, seller
  closing costs, mortgage payoff and other costs you supply.

## Public records (US)

When public records are enabled for the deployment, a US subject is also matched to its county record
by street number and postal code: parcel number, the assessor's facts (beds, baths, living area, lot,
year built), the latest tax bill and assessed value, the last recorded sale, whether the owner lives
there, the open loans with an **estimated remaining balance**, and an automated valuation with
its range and confidence. Only loans still open count: one whose release was recorded, or that a later
refinance paid off, is left out even when the county record still lists it, and a newer refinance the
county record hasn't picked up yet is counted at its amortized balance. The record fills facts the MLS listing lacks (each fact's `sources` entry
says `publicRecord` when it did), the estimated loan balance becomes the net sheet's default
`mortgagePayoff`, and the automated value is returned as `valuation.avm` as a cross-check, never
blended into the comps-based conclusion. Names of owners, borrowers, buyers and sellers are never
returned. `subject.publicRecordStatus` says whether a record was found; Canadian homes are
`unsupported`.

## Allowance

Your brokerage can run **200 CMAs per day** by default (reset at midnight America/Chicago); every
response's `meta.cmaQuota` shows the limit, what's used and when it resets, and the
`x-daily-cma-limit` / `x-daily-cma-remaining` headers carry the same. Reading a stored CMA is free.
Over the allowance you get `429 daily_quota_exceeded` with `Retry-After`. Your CSM can raise it for your
brokerage. Runs are lookup calls (20 per minute) and one runs at a time per brokerage; a second one
started while the first is running gets `429 rate_limited` and a short `Retry-After`.

## A recipe

```bash
curl -s -X POST "$B/cma" "${H[@]}" -H "Content-Type: application/json" -d '{
  "subject": { "address": { "line1": "4182 E Saguaro Bloom Dr", "state": "AZ", "postalCode": "85718" } },
  "overrides": { "condition": "good", "updates": ["kitchen"] },
  "options": { "netSheet": { "mortgagePayoff": 312000 } }
}' | jq '{ value: .data.valuation.indicatedValue, range: .data.valuation.range, list: .data.valuation.recommendedListPrice,
          confidence: .data.valuation.confidence, comps: [.data.soldComps[] | {mls: .property.mlsRecordId, sold: .salePrice, adjusted: .adjustedPrice, flags}],
          proceeds: .data.netSheet.estimatedProceeds }'
```
