Changelog

What shipped

A dated record of what has shipped. Each entry says what the product was doing before and what it does now, in the terms of the work rather than in version numbers.

Last shipped 6 August 2026. Follow by RSS.

Onboarding

The verification link signs you in#

Clicking the link in the verification email used to drop you on the login screen, where you retyped the password you had chosen two minutes earlier. A one-time link that arrived in your own mailbox is evidence enough, so the first click now signs you in and takes you straight to the plan step. Clicking a spent link again, which happens constantly because mail scanners follow links and people open them twice, returns an honest "already verified" instead of the old dead end, whose only offer was a resend that could never mail a verified account anything.

Modeling

Every deal opens on a built model, never a form#

Deals that came in through the upload path already arrived complete. Deals that skipped it, started without a document, or built on an extraction that could not be read, landed on a screen of questions instead. The same completion now runs when the deal page loads, anchored on the stated asking price. Where no cap rate was quoted, income is backed out of the price at a default cap for the asset class. A multifamily unit count scales to the price, and a hotel gets a key count and an average daily rate from a per-key rule of thumb. Those defaults are placeholder assumptions rather than market figures, and every one of them arrives flagged as an estimate at the top of your review pass, so nothing the app filled in reads as something lifted off a document.

Onboarding

Signing up on a phone stops fighting you#

The sign-up and log-in fields are now 16px, which is the threshold below which iOS Safari zooms the page when you tap into one. The first thing a phone signup used to do was pinch back out. Each of those fields also carries a name, which is the second signal a password manager reads when it decides whether to offer autofill. And the consent checkbox no longer disables the submit button outright, so a keyboard or screen-reader user gets the explanatory error rather than a button that silently does nothing.

Interface

Placeholder text stops reading as real data#

An empty Address field drew its placeholder in the same bold weight and full-strength colour as a saved value, so "123 Main St" read as a real and wrong address sitting on your deal. Same for the city, state, and ZIP fields underneath it. Placeholders are lighter and quieter now, matching the sign-up screen.

Reliability

The crash reporter stops reporting a non-crash#

Browsers fire a ResizeObserver loop notification whenever a resize callback schedules more layout. Nothing breaks and nobody sees anything, but it was being counted and reported as a crash, which buried the reports that matter. It is filtered out now.

Onboarding

The plan chooser shows only plans you can buy#

The account panel already filtered the catalog down to the plans that are actually for sale. The chooser you meet immediately after verifying did not, so the first screen a brand-new account saw carried four cards reading "Pricing to be announced" and three add-on cards for a plan the account did not have yet. Both screens run the same filter now, and what they show is what is on pricing.

Reliability

A signup that goes silent now raises an alarm#

A verification email that is delivered and then filed into a junk folder looks, from the sending side, exactly like one that worked: every signal available to the sender reports success, and the only thing that says otherwise is the click that never came. An account that stays unverified past a grace window now raises an alert, once per person. The signup screen also names the junk folder explicitly and offers support@altyst.ai as a way in that does not depend on email finding you.

Accuracy

The document has to agree with itself#

A rent column read as annual when it is monthly, a suite total divided by square feet, an area column read as square feet when it is square metres, an expense line booked monthly as annual: every one of those lands as the same symptom, which is that the income the rent roll implies stops agreeing with the net operating income the document printed a page earlier. There is now a single tie-out that derives NOI from the roll (or from the unit mix) and compares it against the stated figure in both directions, and it names the twelve-times case explicitly, because "off by twelve" tells you exactly where to look. It is deterministic, so it still fires in a workspace with no AI budget left. It stays silent when total operating expenses are missing: without expenses the figure derived from rent is effective gross income rather than NOI, and calling a perfectly consistent document with a 40% expense ratio "1.7x off" would only teach you to dismiss the warning that matters.

Extraction

Rent scale belongs to the whole table#

The annual-versus-monthly call on a unit mix used a flat $10,000 threshold applied value by value, which could split one table down the middle. A mix listing a one-bedroom at $8,400 a year next to a two-bedroom at $19,200 a year converted the two-bedroom to $1,600 a month and left the one-bedroom sitting at $8,400 a month, so two rows of the same table ended up on different scales. It misread both ends of the market for the same reason: workforce housing at $8,400 a year rode in twelve times high, which is the direction that makes a deal look extraordinary and that nobody questions, while a genuine $11,500 a month unit was divided down to $958. Scale is decided once for the whole table now, from the median, and where the mix carries unit sizes the rent per square foot settles it outright.

Extraction

A $245 per foot rent is a rate, not a suite total#

On a commercial roll, the call between an annual suite total and a rate per square foot used a cutoff of $200, which sits deep inside ordinary rate territory and overruled the column's own label. A routine $245 per foot rent on a 1,200 foot suite was re-read as that suite's annual total and divided by its area, so a column that had been labelled correctly arrived at about $0.20 a foot, roughly a thousand times low. In practice the two populations barely overlap, so the cutoff belongs between them rather than at the bottom of one: quoted rates per foot mostly run in the single digits to the low hundreds and reach four figures only in trophy retail, while a suite's annual total starts in the tens of thousands. The cutoff now follows the period the column is quoted in, because a monthly rent column and an annual one sit twelve times apart and one constant cannot serve both.

Extraction

One unreadable cell must not delete the tenant#

A commercial rent roll with "n/a" in a single square-footage cell lost the whole row. The tenant went missing along with its name, its rent, and its lease expiry, none of which were unreadable, and nothing on screen said so: the building quietly lost a tenant and that tenant's income. Whether a row exists is now decided by its identity, and values only decide its contents. An unreadable cell is left blank and named rather than guessed or zeroed, because a rent that cannot be read is not a rent of zero, and a unit whose rent is unknown is not an empty unit.

Reliability

A run that could have succeeded no longer fails#

The deterministic engine is the source of truth for every number, and a live model provider adds a written narrative on top of it, never the arithmetic. But a deployment whose narrator was not installed used to fail inside that provider and show you a failed run, when the finished underwriting, with its provenance and its cross-document checks, had been available the whole time. The run completes now. The operator gets the warning about the misconfiguration instead of you getting a red row on a deal you just uploaded.

Reliability

Errors say something you can act on#

A failed run used to print the underlying exception straight into the Runs panel, so a misconfiguration could show you a Python error naming an internal module and telling you to install a package. Infrastructure faults now get one honest sentence and the full text goes to the log, where it is useful to someone who can act on it. The filter that does this had the opposite failure at first, collapsing sentences you genuinely could act on, such as a purchase price not being set on the deal, into a generic outage notice. Those pass through untouched.

Accuracy

Say it when the returns cannot be real#

Every risk rule judged whether a plausible deal was a good one. None of them asked whether the deal was plausible at all, and a misread input does not announce itself as an error: it produces a model that is internally consistent and completely wrong. A 30.6% going-in cap rate, a 131% levered IRR and an 18.8x equity multiple over a five-year hold were being presented with notes about exit-cap sensitivity, as though the only question left were the exit. The cause of that shape is almost always an input, so the rule names the three that account for most of it: a purchase price missing a digit, a rent column read as annual when it is monthly, and a unit count or square footage off by an order of magnitude. The thresholds sit far outside anything real, at a 25% going-in cap, a 100% levered IRR, and a 10x multiple over a hold of ten years or less, so an opportunistic development underwriting to a 40% IRR never trips them. A rule that cries wolf on good deals is worse than no rule.

Accuracy

A fraction typed into a percent field is a question#

These fields are entered in percent: the box shows 6 next to a % sign for a 6% cap. So a bare 0.06, which is what a spreadsheet stores and what a lot of people type out of habit, was read literally as 0.06% and divided by a hundred a second time. A 0.06% exit cap values the sale at 1,667 times NOI. Both readings parse and both clear every bound, and one of them is catastrophic in silence, so the app now asks and shows you both readings rather than guessing. It only asks on fields where a figure under 1% is not a real number for that field, which is caps, the loan rate, leverage, the expense ratio, and occupancy. Credit loss, concessions, vacancy, and growth rates can legitimately be a fraction of a percent, so 0.5 still commits there without an argument.

Accuracy

The extra digit#

The input bounds existed, in their own comment, to catch a lost sign, a fat-fingered digit, and percent-versus-fraction confusion. They caught the first and the third. A purchase price of $999,999,999,999 committed and underwrote: against a normal property's income it returned a 0.00x DSCR and a 0.0% yield on cost, and a full set of numbers computed from it, with nothing on screen objecting. The new ceilings sit far above any real trade, because a bound that blocks an unusual deal costs more than it saves. A $2.2 billion tower still commits. A trillion-dollar price does not.

Accuracy

A count is a whole number#

24.7 apartments do not exist, but the fraction was accepted and carried into gross potential rent, price per unit, and every per-unit comparison, where it reads as a real figure rather than as a paste that went wrong. A year built of 1987.5 passed for the same reason, because the check only ever looked at the range. Found by sweeping the input parser across the shapes a person actually types, and the rest of that sweep came back clean: decimal commas, doubled separators, a unicode minus, stray text, Infinity and NaN are all refused with a reason, while "$", "1e6", "+5" and a trailing decimal point all parse the way you would want them to.

Modeling

Marking a unit vacant moves the vacancy the engine charges#

Editing the rent roll re-derived everything else the roll defines: the unit count, the average in-place rent, the market rent, rentable area, the renovation count. Not vacancy. So flagging two of ten units vacant moved gross potential rent and left the deal charging whatever vacancy it started with, for the whole hold, with the grid on screen showing two empty units. Flag units vacant and the income came out overstated; mark a vacant unit leased and it stayed understated. Neither said anything. A roll with no vacancy column never participates, so a vacancy the trailing statement established is never zeroed by a roll that does not mention it, and a vacancy you set yourself always wins.

This page is a record, not a roadmap. Every entry describes work that is already merged and deployed, nothing here is a commitment to a future date, and work that nobody outside the codebase would feel does not get an entry.

Run it on a deal you are looking at

Bring the documents you already have.