What We're Building
In this guide, we'll build a complete AI-powered project estimator for construction companies. The application will allow contractors to input project details including scope, materials, and location, then leverage Claude AI to generate accurate cost estimates with material quantities and labor predictions.
Construction projects regularly exceed budgets by 20-30% due to inaccurate initial estimates. Our tool addresses this by combining historical cost data with AI-powered analysis to provide more reliable projections. Key features include:
- Smart project input forms - Capture scope, square footage, materials, and location
- AI-powered cost analysis - Claude analyzes project parameters against industry data
- Material quantity calculations - Automatic material lists with quantities and costs
- Labor cost predictions - Estimate labor hours and costs by trade
- PDF estimate generation - Professional estimates ready for clients
- Historical cost database - Store and learn from past projects
Prerequisites
Before starting this guide, make sure you have the following:
- Node.js 18+ installed on your machine
- Claude API key from Anthropic (sign up at console.anthropic.com)
- Supabase account for the database (free tier works)
- Vercel account for deployment (optional but recommended)
- Basic familiarity with React and TypeScript
- Understanding of REST APIs and async/await patterns
Tech Stack Specification
Here's the technology stack we'll use for this build:
| Layer | Technology | Why This Choice |
|---|---|---|
| Frontend | Next.js 14, Tailwind CSS | Form-heavy UI with excellent DX and built-in routing |
| Backend | Next.js API Routes | Serverless processing, no separate backend needed |
| Database | Supabase (PostgreSQL) | Store cost data, projects, and materials with real-time sync |
| AI/API | Claude API (Anthropic) | Superior reasoning for complex cost analysis |
| PDF Generation | @react-pdf/renderer | Generate professional estimates client-side |
| Deployment | Vercel | Zero-config Next.js deployment with edge functions |
AI Agent Workflow
Here's how to leverage AI tools throughout this build to maximize productivity:
Project Scaffolding with Claude Code
Use Claude Code to scaffold the entire project structure, including the database schema and API routes. This saves hours of boilerplate setup.
# Example prompt for Claude Code
Create a Next.js 14 app with TypeScript for construction cost estimation.
Include:
- Supabase client setup with types
- API routes for /api/estimate and /api/projects
- Form components for project input (scope, materials, location, sqft)
- Database schema for projects, materials, and cost_history tables
- Tailwind config with construction-themed colors (orange/amber accents)
UI Generation with v0.dev
Generate the complex form UI and dashboard components using v0.dev. Construction estimators need professional, form-heavy interfaces that v0 excels at creating.
When prompting v0.dev, specify "construction software" styling - think Procore or Buildertrend aesthetics. Ask for orange/amber accent colors and include specific form fields like "square footage", "project type dropdown", and "material checklist".
Development with Cursor
Use Cursor's AI features to implement the Claude API integration and debug complex estimation logic. The inline editing is particularly useful for refining the cost calculation algorithms.
Step-by-Step Build Guide
Phase 1: Project Setup and Database Schema
Start by creating the Next.js project and setting up the Supabase database with the required tables for storing projects, materials, and historical cost data.
# Create Next.js project
npx create-next-app@14 construction-estimator --typescript --tailwind --app
cd construction-estimator
# Install dependencies
npm install @supabase/supabase-js @anthropic-ai/sdk @react-pdf/renderer
npm install -D @types/node
# Create environment file
touch .env.local
Add your environment variables to .env.local:
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
ANTHROPIC_API_KEY=your_claude_api_key
Phase 2: Cost Database Schema
Create the database schema in Supabase to store project information, material costs, and historical estimates. This data will be used by Claude to improve estimation accuracy over time.
-- Supabase SQL: Create tables for construction estimator
-- Materials reference table with regional pricing
CREATE TABLE materials (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL, -- lumber, concrete, electrical, plumbing, etc.
unit TEXT NOT NULL, -- sqft, linear_ft, each, cubic_yard
base_cost DECIMAL(10,2) NOT NULL,
region TEXT DEFAULT 'national',
updated_at TIMESTAMP DEFAULT NOW()
);
-- Projects table
CREATE TABLE projects (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
project_type TEXT NOT NULL, -- residential, commercial, renovation
square_footage INTEGER NOT NULL,
location TEXT NOT NULL,
scope TEXT,
estimated_cost DECIMAL(12,2),
actual_cost DECIMAL(12,2),
status TEXT DEFAULT 'draft',
created_at TIMESTAMP DEFAULT NOW()
);
-- Cost line items for each project
CREATE TABLE estimate_items (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
project_id UUID REFERENCES projects(id),
category TEXT NOT NULL,
description TEXT NOT NULL,
quantity DECIMAL(10,2),
unit TEXT,
unit_cost DECIMAL(10,2),
total_cost DECIMAL(12,2),
is_labor BOOLEAN DEFAULT FALSE
);
-- Insert sample material data
INSERT INTO materials (name, category, unit, base_cost) VALUES
('2x4 Lumber', 'lumber', 'linear_ft', 0.85),
('Concrete', 'concrete', 'cubic_yard', 150.00),
('Drywall 4x8', 'drywall', 'sheet', 15.50),
('Roofing Shingles', 'roofing', 'square', 95.00),
('Electrical Wire 12/2', 'electrical', 'linear_ft', 0.65);
Phase 3: AI-Powered Cost Estimation
Create the API route that sends project details to Claude for intelligent cost analysis. Claude will analyze the scope, compare against industry standards, and return a detailed breakdown.
// app/api/estimate/route.ts
import Anthropic from '@anthropic-ai/sdk';
import { createClient } from '@supabase/supabase-js';
import { NextResponse } from 'next/server';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export async function POST(request: Request) {
const { projectType, squareFootage, location, scope, materials } =
await request.json();
// Fetch current material costs from database
const { data: materialCosts } = await supabase
.from('materials')
.select('*');
// Build the estimation prompt for Claude
const estimationPrompt = `You are an expert construction cost estimator.
Analyze this project and provide a detailed cost breakdown.
PROJECT DETAILS:
- Type: ${projectType}
- Square Footage: ${squareFootage} sq ft
- Location: ${location}
- Scope: ${scope}
- Selected Materials: ${materials.join(', ')}
CURRENT MATERIAL COSTS (use these as baseline):
${JSON.stringify(materialCosts, null, 2)}
Provide a JSON response with this structure:
{
"summary": "Brief project summary",
"totalEstimate": number,
"confidenceLevel": "low" | "medium" | "high",
"materials": [
{
"item": "Material name",
"quantity": number,
"unit": "unit type",
"unitCost": number,
"totalCost": number,
"notes": "Any relevant notes"
}
],
"labor": [
{
"trade": "Trade name (e.g., Framing, Electrical)",
"hours": number,
"hourlyRate": number,
"totalCost": number
}
],
"additionalCosts": [
{
"item": "Permits, inspections, etc.",
"cost": number
}
],
"recommendations": ["List of cost-saving recommendations"],
"risks": ["Potential cost risks to consider"]
}`;
const message = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages: [
{
role: 'user',
content: estimationPrompt,
},
],
});
// Parse Claude's response
const responseText = message.content[0].type === 'text'
? message.content[0].text
: '';
const estimate = JSON.parse(responseText);
return NextResponse.json(estimate);
}
Phase 4: Material Quantity Calculations
Build the form component that captures project details and displays the AI-generated estimate with material quantities. The form uses controlled components for validation.
// components/EstimateForm.tsx
'use client';
import { useState } from 'react';
interface EstimateFormProps {
onEstimateGenerated: (estimate: any) => void;
}
export default function EstimateForm({ onEstimateGenerated }: EstimateFormProps) {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
projectType: 'residential',
squareFootage: '',
location: '',
scope: '',
materials: [] as string[],
});
const projectTypes = [
{ value: 'residential', label: 'Residential New Build' },
{ value: 'commercial', label: 'Commercial Construction' },
{ value: 'renovation', label: 'Renovation/Remodel' },
{ value: 'addition', label: 'Home Addition' },
];
const materialOptions = [
'Standard Lumber', 'Premium Lumber', 'Concrete Foundation',
'Steel Framing', 'Standard Roofing', 'Metal Roofing',
'Basic Electrical', 'Smart Home Wiring', 'Standard Plumbing',
];
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch('/api/estimate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const estimate = await response.json();
onEstimateGenerated(estimate);
} catch (error) {
console.error('Estimation failed:', error);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Project Type */}
<div>
<label className="block text-sm font-medium mb-2">
Project Type
</label>
<select
value={formData.projectType}
onChange={(e) => setFormData({...formData, projectType: e.target.value})}
className="w-full p-3 border rounded-lg bg-gray-900"
>
{projectTypes.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
{/* Square Footage */}
<div>
<label className="block text-sm font-medium mb-2">
Square Footage
</label>
<input
type="number"
value={formData.squareFootage}
onChange={(e) => setFormData({...formData, squareFootage: e.target.value})}
placeholder="e.g., 2500"
className="w-full p-3 border rounded-lg bg-gray-900"
required
/>
</div>
{/* Location */}
<div>
<label className="block text-sm font-medium mb-2">
Project Location
</label>
<input
type="text"
value={formData.location}
onChange={(e) => setFormData({...formData, location: e.target.value})}
placeholder="City, State"
className="w-full p-3 border rounded-lg bg-gray-900"
required
/>
</div>
{/* Scope Description */}
<div>
<label className="block text-sm font-medium mb-2">
Project Scope
</label>
<textarea
value={formData.scope}
onChange={(e) => setFormData({...formData, scope: e.target.value})}
placeholder="Describe the project scope, special requirements, finishes..."
rows={4}
className="w-full p-3 border rounded-lg bg-gray-900"
/>
</div>
{/* Material Selection */}
<div>
<label className="block text-sm font-medium mb-2">
Materials & Features
</label>
<div className="grid grid-cols-2 gap-2">
{materialOptions.map((material) => (
<label key={material} className="flex items-center gap-2 p-2">
<input
type="checkbox"
checked={formData.materials.includes(material)}
onChange={(e) => {
const updated = e.target.checked
? [...formData.materials, material]
: formData.materials.filter((m) => m !== material);
setFormData({...formData, materials: updated});
}}
/>
{material}
</label>
))}
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 px-6 bg-amber-500 hover:bg-amber-600
text-white font-semibold rounded-lg transition-colors
disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Generating Estimate...' : 'Generate AI Estimate'}
</button>
</form>
);
}
Phase 5: Labor Cost Predictions
Enhance the Claude prompt to include detailed labor cost predictions by trade. This uses industry-standard labor rates adjusted for location.
// Enhanced labor estimation prompt addition
const laborPromptSection = `
LABOR ESTIMATION GUIDELINES:
Calculate labor hours based on these industry standards:
- Framing: 0.04 hours per sq ft
- Electrical: 0.03 hours per sq ft
- Plumbing: 0.025 hours per sq ft
- Drywall: 0.02 hours per sq ft
- Painting: 0.015 hours per sq ft
- Roofing: 0.035 hours per sq ft
Apply location multipliers:
- High cost areas (CA, NY, MA): 1.3x
- Medium cost areas (TX, FL, CO): 1.0x
- Lower cost areas (OH, TN, NC): 0.85x
Base hourly rates by trade:
- General Labor: $25/hr
- Carpenter/Framer: $35/hr
- Electrician: $55/hr
- Plumber: $50/hr
- HVAC: $48/hr
- Roofer: $40/hr
Include 15% labor burden for insurance, taxes, and benefits.
`;
Phase 6: PDF Estimate Generation
Create a professional PDF export using @react-pdf/renderer. This generates client-ready estimates that contractors can share directly.
// components/EstimatePDF.tsx
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';
const styles = StyleSheet.create({
page: { padding: 40, fontFamily: 'Helvetica' },
header: { marginBottom: 30 },
title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 },
subtitle: { fontSize: 12, color: '#666' },
section: { marginBottom: 20 },
sectionTitle: { fontSize: 14, fontWeight: 'bold', marginBottom: 10,
borderBottom: '1px solid #ddd', paddingBottom: 5 },
row: { flexDirection: 'row', borderBottom: '1px solid #eee',
paddingVertical: 8 },
col: { flex: 1 },
colRight: { flex: 1, textAlign: 'right' },
total: { flexDirection: 'row', marginTop: 20, paddingTop: 10,
borderTop: '2px solid #333' },
totalLabel: { flex: 1, fontSize: 16, fontWeight: 'bold' },
totalValue: { flex: 1, fontSize: 16, fontWeight: 'bold', textAlign: 'right' },
});
interface EstimatePDFProps {
estimate: {
summary: string;
totalEstimate: number;
materials: Array<{item: string; quantity: number; unit: string; totalCost: number}>;
labor: Array<{trade: string; hours: number; totalCost: number}>;
};
projectName: string;
}
export function EstimatePDF({ estimate, projectName }: EstimatePDFProps) {
return (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<Text style={styles.title}>Project Estimate</Text>
<Text style={styles.subtitle}>{projectName}</Text>
<Text style={styles.subtitle}>
Generated: {new Date().toLocaleDateString()}
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Materials</Text>
{estimate.materials.map((item, i) => (
<View key={i} style={styles.row}>
<Text style={styles.col}>{item.item}</Text>
<Text style={styles.col}>{item.quantity} {item.unit}</Text>
<Text style={styles.colRight}>
${item.totalCost.toLocaleString()}
</Text>
</View>
))}
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Labor</Text>
{estimate.labor.map((item, i) => (
<View key={i} style={styles.row}>
<Text style={styles.col}>{item.trade}</Text>
<Text style={styles.col}>{item.hours} hrs</Text>
<Text style={styles.colRight}>
${item.totalCost.toLocaleString()}
</Text>
</View>
))}
</View>
<View style={styles.total}>
<Text style={styles.totalLabel}>Total Estimate</Text>
<Text style={styles.totalValue}>
${estimate.totalEstimate.toLocaleString()}
</Text>
</View>
</Page>
</Document>
);
}
Common Issues & Solutions
Here are some common issues you might encounter and how to solve them:
If Claude returns malformed JSON, wrap the parsing in a try/catch and add instructions to your prompt like "Respond ONLY with valid JSON, no markdown code blocks". You can also use Claude's JSON mode for more reliable structured output.
@react-pdf/renderer needs to run client-side. Use dynamic imports with `ssr: false` if you're having issues: `const PDFDownloadLink = dynamic(() => import('@react-pdf/renderer').then(mod => mod.PDFDownloadLink), { ssr: false })`
The Claude API has rate limits. Implement request queuing for production use, and consider caching estimates for similar project parameters to reduce API calls.
Other common issues include Supabase connection timeouts (increase pool size), Vercel function timeouts for complex estimates (use streaming responses), and material cost data becoming stale (implement a scheduled update job).
Next Steps
You now have a working AI-powered construction estimator. Here are some ways to extend it:
- Add user authentication - Use Supabase Auth to let contractors save and manage multiple projects
- Implement estimate comparison - Show side-by-side comparisons of different material/scope options
- Build a dashboard - Track estimate accuracy over time by comparing estimates to actual costs
- Add subcontractor bidding - Let subs submit bids that feed into the estimation system
- Integrate with accounting - Connect to QuickBooks or other accounting software for seamless project setup
The real power comes from feeding actual project costs back into the system. Build a feedback loop where completed projects update your material costs and labor multipliers, making Claude's estimates more accurate for your specific market.
Follow the Vibe Coding Enthusiast
Follow JD — product updates on LinkedIn, personal takes on X.