February 19, 2026 · 9 min
AI-Powered Classified Ads: How AI Improves User Engagement
How I used AI to transform a traditional classified ads platform — from smart listing generation to personalized recommendations and fraud detection.
AI-Powered Classified Ads: How AI Improves User Engagement
TL;DR: Traditional classified ad platforms treat every user the same and rely on users to write their own listings. AI changes both of these fundamentally. Here's how I used AI to increase engagement and listing quality on a classified ads platform.
The Classified Ads Problem
Traditional platforms like Craigslist have barely changed in 25 years. The core user experience problems:
- Low listing quality — sellers write vague, inconsistent listings
- Poor discoverability — search is keyword-only, no semantic understanding
- No personalization — same results for every user
- Fraud and spam — easy to post fake listings
- Stale listings — outdated posts clutter search results
AI provides practical solutions to all five.
Feature 1: AI-Assisted Listing Creation
Most sellers don't know how to write a compelling listing. The AI assistant guides them:
// features/listings/actions/generate-listing.ts
'use server';
import { openai } from '@/lib/openai';
interface ListingGenerationInput {
category: string;
userDescription: string; // What the user typed, usually a few sentences
uploadedImages?: string[]; // Image URLs for vision analysis
priceHint?: string;
}
export async function generateListingContent(
input: ListingGenerationInput
): Promise<GeneratedListing> {
const systemPrompt = `You are an expert at writing compelling classified ad listings.
Generate professional, honest, and detailed listings that attract serious buyers.
Guidelines:
- Title: Clear, searchable, 8-12 words
- Description: Highlight key features, condition, usage history
- Include relevant details for the ${input.category} category
- Suggest fair price range based on market knowledge
- Generate relevant tags for discoverability`;
const userPrompt = `Create a listing for: "${input.userDescription}"
Category: ${input.category}
Price hint they mentioned: ${input.priceHint || 'not specified'}
Return valid JSON with: title, description, suggestedPrice, tags[], condition`;
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
response_format: { type: 'json_object' },
});
const generated = JSON.parse(response.choices[0].message.content!);
// Validate generated content
return GeneratedListingSchema.parse(generated);
}
Results: Listings generated with AI assistance get 2.3× more views and 1.8× more contact requests than manually written listings of similar items.
Feature 2: Image-to-Listing (Multimodal AI)
Users can now just upload photos and the AI writes the listing:
export async function generateFromImages(
imageUrls: string[],
category: string
): Promise<GeneratedListing> {
// Build vision prompt with image URLs
const imageMessages = imageUrls.map(url => ({
type: 'image_url' as const,
image_url: { url, detail: 'high' as const }
}));
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{
role: 'user',
content: [
...imageMessages,
{
type: 'text',
text: `Analyze these photos and create a classified ad listing for the ${category} category.
Identify: item name, condition (new/excellent/good/fair/poor), visible features,
any visible wear or damage, estimated age if apparent.
Return JSON: { title, description, condition, estimatedAge, tags[], concerns[] }`
}
]
}],
response_format: { type: 'json_object' },
});
return JSON.parse(response.choices[0].message.content!);
}
Feature 3: Semantic Search
Keyword search misses the intent behind queries. Semantic search understands it:
User searches: "transport my dog on weekends"
Keyword search returns: listings containing "dog", "transport", "weekends" literally
Semantic search returns: pet carriers, dog crates, car seat covers, dog seat belts, kennels
Implementation using vector embeddings:
// When a listing is created, embed it
async function embedListing(listing: Listing): Promise<void> {
const textToEmbed = `
${listing.title}
${listing.description}
Category: ${listing.category}
Tags: ${listing.tags.join(', ')}
Condition: ${listing.condition}
`;
const embedding = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: textToEmbed,
});
// Store embedding in PostgreSQL with pgvector extension
await prisma.$executeRaw`
UPDATE listings
SET embedding = ${embedding.data[0].embedding}::vector
WHERE id = ${listing.id}
`;
}
// Semantic search query
async function semanticSearch(
query: string,
filters: SearchFilters,
limit = 20
): Promise<Listing[]> {
// Embed the search query
const queryEmbedding = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
});
const vector = queryEmbedding.data[0].embedding;
// Find semantically similar listings using cosine similarity
const results = await prisma.$queryRaw<Listing[]>`
SELECT *, 1 - (embedding <=> ${vector}::vector) AS similarity
FROM listings
WHERE
category = ${filters.category}
AND price BETWEEN ${filters.minPrice} AND ${filters.maxPrice}
AND deleted_at IS NULL
AND expires_at > NOW()
ORDER BY embedding <=> ${vector}::vector
LIMIT ${limit}
`;
return results.filter(r => r.similarity > 0.5); // Minimum relevance threshold
}
Feature 4: Personalized Recommendations
The recommendation engine analyzes user behavior to surface relevant listings:
class RecommendationEngine {
async getRecommendations(userId: string): Promise<Listing[]> {
// Get user's interaction history
const interactions = await prisma.userInteraction.findMany({
where: { userId },
orderBy: { timestamp: 'desc' },
take: 50,
include: { listing: true },
});
if (interactions.length < 3) {
// Cold start: return popular listings in user's location
return this.getPopularListings(userId);
}
// Build user preference vector from viewed/saved listings
const preferenceVector = await this.buildPreferenceVector(
interactions.map(i => i.listing)
);
// Find listings similar to user's preference vector
return prisma.$queryRaw`
SELECT l.*,
1 - (l.embedding <=> ${preferenceVector}::vector) AS relevance_score
FROM listings l
WHERE
l.user_id != ${userId}
AND l.id NOT IN (SELECT listing_id FROM user_interactions WHERE user_id = ${userId})
AND l.expires_at > NOW()
ORDER BY l.embedding <=> ${preferenceVector}::vector
LIMIT 20
`;
}
private async buildPreferenceVector(viewedListings: Listing[]): Promise<number[]> {
// Weight recent interactions more heavily
const weightedEmbeddings = await Promise.all(
viewedListings.map(async (listing, idx) => {
const weight = 1 / (idx + 1); // More recent = higher weight
const embedding = await this.getListingEmbedding(listing.id);
return embedding.map(v => v * weight);
})
);
// Average the weighted embeddings
const sumVector = weightedEmbeddings.reduce(
(acc, emb) => acc.map((v, i) => v + emb[i]),
new Array(1536).fill(0)
);
const totalWeight = viewedListings.reduce((acc, _, idx) => acc + 1 / (idx + 1), 0);
return sumVector.map(v => v / totalWeight);
}
}
Feature 5: AI Fraud Detection
Detecting fake or scam listings is critical for platform trust:
async function scoreListing(listing: ListingInput): Promise<FraudScore> {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{
role: 'user',
content: `Analyze this classified ad listing for potential fraud indicators.
Title: ${listing.title}
Description: ${listing.description}
Price: $${listing.price} (Category avg: $${listing.categoryAvgPrice})
Contact: ${listing.contactMethod}
Return JSON with:
- fraudScore: 0-100 (100 = definitely fraudulent)
- flags: string[] (specific concerns found)
- recommendation: "approve" | "review" | "reject"
Common fraud signals:
- Price far below market value
- Urgency pressure ("must sell today")
- Asking for wire transfer/gift cards
- Too-good-to-be-true descriptions
- Generic stock photo language
- Requests to communicate off-platform`
}],
response_format: { type: 'json_object' },
});
const result = JSON.parse(response.choices[0].message.content!);
// Auto-reject high-confidence fraud
if (result.fraudScore > 85) {
await flagListingForRemoval(listing.id, result.flags);
}
// Queue for human review if uncertain
if (result.fraudScore > 50) {
await addToModerationQueue(listing.id, result);
}
return result;
}
Feature 6: Smart Pricing Suggestions
async function suggestPrice(
listing: ListingInput,
category: string
): Promise<PriceSuggestion> {
// Get recently sold similar items for reference
const comparables = await semanticSearch(
`${listing.title} ${listing.condition}`,
{ category, status: 'sold' },
10
);
const priceRange = comparables.length > 0
? {
min: Math.min(...comparables.map(l => l.price)),
max: Math.max(...comparables.map(l => l.price)),
avg: comparables.reduce((s, l) => s + l.price, 0) / comparables.length,
}
: null;
const suggestion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{
role: 'user',
content: `Suggest a price for this classified listing:
Item: ${listing.title}
Condition: ${listing.condition}
Description: ${listing.description}
Recently sold comparable range: ${JSON.stringify(priceRange)}
Return JSON: { suggestedPrice, minPrice, maxPrice, reasoning }`
}],
response_format: { type: 'json_object' },
});
return JSON.parse(suggestion.choices[0].message.content!);
}
Engagement Impact
After launching AI features:
| Metric | Before AI | After AI | Change | |--------|-----------|----------|--------| | Time to create listing | 8 min avg | 2.5 min avg | -69% | | Listing completion rate | 52% | 84% | +62% | | Search result relevance (satisfaction) | 58% | 82% | +41% | | Fraud reports per 1000 listings | 12 | 3 | -75% | | Return visits per user | 2.1/week | 3.7/week | +76% |