If you are asked to build or extend transaction management real estate software, the first surprise is that the hard part is not the UI. It is modelling a deal so that a managing broker can later prove it was supervised correctly. That requirement shapes the schema, the document pipeline and especially how you let AI touch anything.
This post walks through the pieces we would design first. The business case (buy, customise or build) is covered in our real estate transaction management software guide; here we stay in the code.
1. The core data model
A transaction is not a row with a status column. It has sides (listing, buyer, or both), parties with roles, an office and brand, a jurisdiction that drives compliance rules, and documents that satisfy checklist items.
A trimmed TypeScript sketch, illustrative only:
type Side = "listing" | "buyer" | "dual";
interface Transaction {
id: string;
tenantId: string; // brand or franchisee, isolate from day one
officeId: string;
side: Side;
jurisdiction: string; // e.g. "US-TX"
propertyType: "sfr" | "condo" | "land" | "multifamily";
financing: "cash" | "conventional" | "fha" | "va" | "other";
listingKey?: string; // RESO ListingKey when sourced from MLS
stage: "pre_contract" | "under_contract" | "closed" | "cancelled";
}
interface ChecklistItem {
id: string;
transactionId: string;
requirementId: string; // points at a versioned rule
status: "missing" | "submitted" | "returned" | "approved" | "waived";
documentIds: string[];
}
Two decisions pay off later. First, checklist requirements are resolved from a rules table keyed on jurisdiction, side, property type and financing, and those rules are versioned. When a regulation changes, such as the written buyer representation agreements required after the 2024 NAR settlement practice changes, compliance admins add a rule version rather than a developer shipping a migration. Second, approved can only be set by a human principal. Enforce that in the domain layer, not just the UI.
2. Pulling listing data over the RESO Web API
Most MLSs now expose data through the RESO Web API, an OData-based standard with normalised field names like ListingKey, UnparsedAddress, ListPrice and StandardStatus. Seeding a transaction from the MLS removes a whole class of address and price typos.
GET /odata/Property?$filter=ListingId eq 'X1234567'
&$select=ListingKey,UnparsedAddress,City,StateOrProvince,PostalCode,ListPrice,StandardStatus
Authorization: Bearer <token>
Store MLS values as a timestamped snapshot, since listing data changes and the contract does not. Treat each MLS as its own integration with separate credentials, rate limits and licensing terms, and never overwrite fields a human has already confirmed.
3. The document pipeline
Agents upload a single 40-page PDF containing the purchase agreement, several addenda and a disclosure. Your pipeline should be asynchronous and idempotent, with each stage writing its output and evidence.
upload -> store original (immutable, hashed)
-> split pages -> OCR / text layer
-> classify each segment against form library
-> map to checklist items
-> run deterministic checks, then model checks
-> write findings with evidence -> human review queue
Keep original bytes immutable and addressed by content hash (for example SHA-256). Derived artifacts such as split documents, OCR text and extractions reference that hash plus a page range, and re-uploads stack as versions.
Run deterministic rules first. If a known form template has a blank mandatory field, a coordinate-based rule is cheaper and more reliable than a model. Save the model for classification of unfamiliar addenda and cross-document reasoning.
4. Grounded AI extraction with human review
Contract dates drive the timeline: acceptance, inspection end, financing contingency, earnest money and closing. Extraction is a good fit for a model, as long as every value is grounded.
Ask for structured output that includes evidence:
{
"field": "inspection_period_end",
"value": "2026-10-14",
"rule": "10 calendar days after acceptance",
"evidence": { "documentHash": "9f2c...", "page": 3, "quote": "within ten (10) days" },
"confidence": 0.86
}
Then validate before persisting:
- The quoted text must actually appear on the cited page of the cited document. If not, discard the value and raise a flag.
- Compute the deadline in code from the grounded rule and acceptance date, applying business-day or calendar-day logic per the contract. Do not trust model date arithmetic.
- Route anything under a per-field confidence threshold to a person. Even above the threshold, a coordinator confirms before the date drives reminders.
The model proposes; a human accepts, edits or rejects, and that response becomes labelled evaluation data.
5. An append-only audit log
The audit log is the product. Make it append-only and write to it from the domain layer on every meaningful event.
CREATE TABLE audit_event (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL,
transaction_id UUID NOT NULL,
actor_type TEXT NOT NULL, -- 'user' | 'system' | 'model'
actor_id TEXT NOT NULL,
action TEXT NOT NULL, -- 'document.uploaded', 'checklist.approved', ...
payload JSONB NOT NULL, -- includes model + prompt version for AI events
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
prev_hash TEXT,
hash TEXT NOT NULL
);
For AI events, record model identifier and version, prompt template version, raw output, confidence and the subsequent human decision. Chaining each row's hash to the previous one makes tampering detectable. Revoke UPDATE and DELETE on the table for the application role.
Where to go next
The same event stream that feeds the audit log can feed CRM, accounting and a data warehouse, so external systems subscribe instead of polling. If you are scoping a build like this, or bolting an AI review layer onto an existing platform, our custom software development team has written up how we structure that work.
Frequently Asked Questions
What is the RESO Web API?
It is the real estate industry's standard API for MLS data, built on OData, with normalised field names defined by the RESO Data Dictionary. It replaced older RETS feeds for most MLSs and makes listing data much easier to consume consistently.
Why store original documents by content hash?
Hashing makes originals immutable and verifiable. Derived outputs like split pages, OCR text and extracted fields can reference the exact bytes they came from, which keeps evidence intact for audits and dispute resolution.
Should date calculations be done by the language model?
No. Let the model extract the contractual rule and quote its source, then compute the actual deadline in deterministic code that handles calendar versus business days and holidays correctly.
How do you prevent AI from approving checklist items?
Enforce it in the domain layer: only authenticated human principals with the right role can transition an item to approved. Model and system actors can only submit findings, flag or return documents.
What belongs in an audit event for an AI action?
The model identifier and version, prompt template version, the raw output and evidence, the confidence score, and a link to the human decision that followed. That is enough to reconstruct what a reviewer was shown.


Top comments (0)