

As NYC moves toward mandating transparency in AI-generated property descriptions, real estate platforms must implement programmatic ways to detect and flag AI-generated content. This guide demonstrates how to build a "Disclosure Engine" that analyzes listing text and automatically attaches the required legal metadata.
Before coding, ensure you have the following:
is_ai_generated flag (e.g., PostgreSQL, MongoDB).Open your terminal and run the following commands to set up your development environment.
# Create a virtual environment
python -m venv venv
source venv/bin/activate # On Windows use: venv\Scripts\activate
# Install required libraries
pip install openai python-dotenv pydantic fastapi uvicorn
# Initialize project
npm init -y
# Install dependencies
npm install openai dotenv zod express
npm install --save-dev typescript @types/node @types/express ts-node
The goal is to create a function that accepts a listing description and returns a structured object containing the text and a mandatory disclosure flag.
This approach uses Pydantic for strict data validation, ensuring the disclosure flag is always present.
import os
from typing import Dict
from openai import OpenAI
from dotenv import load_dotenv
from pydantic import BaseModel, Field
# Load environment variables
load_dotenv()
# Initialize Client
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
class ListingAnalysis(BaseModel):
"""Schema for the processed listing."""
original_text: str
is_ai_generated: bool = Field(description="True if the text shows signs of AI generation")
confidence_score: float = Field(description="0.0 to 1.0 score of AI probability")
disclosure_notice: str = Field(description="The mandatory NYC legal disclaimer")
def analyze_listing(description: str) -> ListingAnalysis:
"""
Analyzes a property description to determine if AI was used.
"""
try:
prompt = (
"Analyze the following real estate listing. Determine if it was likely "
"written by an AI. Return ONLY a JSON object with: "
"'is_ai_generated' (boolean), 'confidence_score' (float), and "
"'disclosure_notice' (string). "
"If AI is detected, the notice should be: 'This listing was generated with AI assistance.'"
)
response = client.chat.completions.create(
model="gpt-4o-mini", # Using a cost-effective model for classification
messages=[
{"role": "system", "content": "You are a compliance officer for NYC real estate."},
{"role": "user", "content": f"{prompt}\n\nListing: {description}"}
],
response_format={"type": "json_object"}
)
# Parse the JSON response
import json
data = json.loads(response.choices[0].message.content)
return ListingAnalysis(
original_text=description,
is_ai_generated=data['is_ai_generated'],
confidence_score=data['confidence_score'],
disclosure_notice=data.get('disclosure_notice', "")
)
except Exception as e:
print(f"Error during analysis: {e}")
# Fallback: Default to safe (disclose if unsure)
return ListingAnalysis(
original_text=description,
is_ai_generated=True,
confidence_score=0.0,
disclosure_notice="Compliance Error: Manual review required."
)
# --- Test Execution ---
if __name__ == "__main__":
test_desc = "Experience luxury living in this stunning penthouse with floor-to-ceiling windows..."
result = analyze_listing(test_desc)
print(f"AI Detected: {result.is_ai_generated}")
print(f"Notice: {result.disclosure_notice}")
This implementation uses Zod for schema validation, which is industry standard for TypeScript applications.
import OpenAI from 'openai';
import 'dotenv/config';
import { z } from 'zod';
// 1. Define the Schema for strict compliance
const ListingSchema = z.object({
isAiGenerated: z.boolean(),
confidenceScore: z.number().min(0).max(1),
disclosureNotice: z.string(),
});
type ListingResult = z.infer<typeof ListingSchema>;
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
/**
* Analyzes property text for AI markers to comply with NYC regulations.
* @param description The raw text from the real estate listing.
*/
async function processListingCompliance(description: string): Promise<ListingResult | null> {
try {
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content: "You are a regulatory compliance tool. Analyze text for AI markers."
},
{
role: "user",
content: `Analyze this listing: "${description}". Return JSON: { "isAiGenerated": boolean, "confidenceScore": number, "disclosureNotice": string }. If AI is suspected, use: 'AI-generated content detected.'`
}
],
response_format: { type: "json_object" },
});
const rawContent = response.choices[0].message.content;
if (!rawContent) throw new Error("Empty response from AI");
// 2. Validate the AI output against our schema
const parsed = JSON.parse(rawContent);
return ListingSchema.parse(parsed);
} catch (error) {
console.error("Compliance Check Failed:", error);
// 3. Fail-safe: If error occurs, we flag for manual review to avoid legal risk
return {
isAiGenerated: true,
confidenceScore: 0,
disclosureNotice: "Manual Review Required (System Error)"
};
}
}
// --- Test Execution ---
const sampleListing = "This breathtaking apartment offers unparalleled views of the skyline...";
processListingCompliance(sampleListing).then(console.log);
Create a .env file in your root directory. Never commit this file to version control.
# API Keys
OPENAI_API_KEY=sk-proj-your-actual-key-here
# Environment Settings
NODE_ENV=development
PYTHON_ENV=dev
# Compliance Thresholds
# If confidence > 0.7, auto-apply disclosure
AI_DETECTION_THRESHOLD=0.7
In legal compliance, it is better to over-disclose than to under-disclose.
Incorrect Pattern:
if (ai_detected) { show_label() } else { hide_label() }
Correct (Production) Pattern:
if (ai_detected || confidence_score < threshold) { show_label() }
In a real production app, you wouldn't call this in the UI. You would run this as a background worker (like Celery in Python or BullMQ in Node) whenever a realtor hits "Save" on a listing.
| Error | Cause | Fix |
|---|---|---|
AuthenticationError | Invalid or expired API Key | Check .env and ensure the key has billing enabled. |
ValidationError (Zod/Pydantic) | AI returned unexpected JSON format | Use response_format: { type: "json_object" } and tighten your system prompt. |
RateLimitError | Too many listings being processed at once | Implement exponential backoff or use a task queue. |
TimeoutError | Listing description is too long | Truncate the text to the first 1,000 characters before sending to the API. |
confidence_score and the timestamp of the check for legal defense?disclosure_notice string matches the exact wording required by NYC law?Source: Hacker News Best
Follow ICARAX for more AI insights and tutorials.
