We Built Free AI Tools for Contractors (Visibility Scan + Profit Calculator)
We Built Free AI Tools for Contractors (Visibility Scan + Profit Calculator)
TL;DR: We built two free AI-powered tools for contractors: a Visibility Scan that shows how invisible they are online (and who’s hijacking their brand), and a Profit Calculator that reveals their true hourly wage after hidden costs. These tools tell the brutal truth—and show contractors exactly why they need a professional website.
The Problem: Contractors Are Getting Crushed
We talked to hundreds of contractors. The same problems kept coming up:
Problem 1: “I don’t show up when people search for me”
- Customer searches “Joe’s Plumbing San Diego”
- Yelp, HomeAdvisor, and Thumbtack show up first
- Joe’s actual website (if he has one) is buried on page 2
- Directories profit from Joe’s reputation while he loses customers
Problem 2: “I keep losing money on jobs”
- Contractor quotes $5,000 for a job
- Materials cost $2,000
- 40 hours of work with 2-person crew
- After overhead and taxes: $18.75/hour effective wage
- Contractor thinks he made good money. He didn’t.
The insight: Contractors don’t know what they don’t know. They need tools that show them the brutal truth.
Tool 1: Visibility Scan
What It Does
The Visibility Scan checks a contractor’s online presence across four dimensions:
- Google Maps Presence: Are you on Google Maps? Is your listing claimed?
- Website Presence: Do you have a real website (not just a Facebook page)?
- Search Ranking: When someone searches “plumber in San Diego”, where do you rank?
- Brand Control: When someone searches your business name, do YOU show up first?
The Technical Implementation
export async function POST(request: NextRequest) {
const { businessName, trade, location } = await request.json();
// Run all checks in parallel for speed
const [mapsCheck, websiteCheck, rankingCheck, reputationCheck] = await Promise.all([
checkGoogleMapsPresence(businessName, location),
checkWebsitePresence(businessName),
checkSearchRanking(businessName, trade, location),
checkReputation(businessName, location)
]);
// Calculate final results
const result = calculateResults(
mapsCheck,
websiteCheck,
rankingCheck,
reputationCheck,
businessName,
trade,
location
);
return NextResponse.json(result);
}
Google Maps Check
We search Google for the business and check if they have a Maps listing:
async function checkGoogleMapsPresence(businessName: string, location: string): Promise<{
found: boolean;
claimed: boolean;
url?: string;
details: string;
}> {
const searchQuery = `${businessName} ${location}`;
const results = await performGoogleSearch(searchQuery);
// Look for Google Maps results
const mapsResult = results.find(r =>
r.link?.includes('google.com/maps') ||
r.link?.includes('maps.google.com')
);
if (mapsResult) {
// Check if listing appears claimed (has photos, reviews, hours)
const claimed = mapsResult.snippet?.includes('reviews') ||
mapsResult.snippet?.includes('hours');
return {
found: true,
claimed,
url: mapsResult.link,
details: claimed
? "Found and claimed! Make sure photos and hours are up to date."
: "Found but appears unclaimed. Claim it to add photos and respond to reviews!"
};
}
return {
found: false,
claimed: false,
details: "NOT FOUND on Google Maps. 46% of all Google searches are local. You're invisible to nearly half of potential customers."
};
}
Website Check
We search for the business and filter out directory sites:
async function checkWebsitePresence(businessName: string): Promise<{
found: boolean;
url: string | null;
details: string;
}> {
const websiteResults = await performGoogleSearch(`"${businessName}" official website`);
const directResults = await performGoogleSearch(`"${businessName}" contact us`);
const allResults = [...websiteResults, ...directResults];
// Filter out directory sites
const directoryDomains = [
'yelp.com', 'yellowpages.com', 'facebook.com', 'linkedin.com',
'bbb.org', 'angieslist.com', 'homeadvisor.com', 'thumbtack.com',
'google.com', 'instagram.com', 'twitter.com', 'nextdoor.com'
];
const websiteResult = allResults.find(r => {
if (!r.link) return false;
const domain = new URL(r.link).hostname.toLowerCase();
return !directoryDomains.some(d => domain.includes(d));
});
if (websiteResult) {
return {
found: true,
url: websiteResult.link,
details: `Found website: ${websiteResult.link}. Make sure it's mobile-friendly and loads fast!`
};
}
// Check if they at least have directory presence
const hasDirectoryPresence = allResults.some(r =>
r.link?.includes('yelp.com') ||
r.link?.includes('facebook.com')
);
return {
found: false,
url: null,
details: hasDirectoryPresence
? "No dedicated website found, but you have some directory listings. A professional website would significantly boost your visibility."
: "No website found. 97% of customers search online before calling. You're invisible to them without a website."
};
}
Brand Hijacking Check
This is the brutal one. We search for the business name and see who shows up first:
async function checkReputation(businessName: string, location: string): Promise<{
found: boolean;
details: string;
hijackedBy?: string;
}> {
const results = await performGoogleSearch(`"${businessName}" ${location}`);
if (results.length === 0) {
return {
found: false,
details: `CRITICAL: When people search for "${businessName}", ABSOLUTELY NOTHING comes up. To a modern customer, you do not exist. You are losing 100% of word-of-mouth referrals who try to verify you.`
};
}
const firstResult = results[0];
const directoryDomains = [
'yelp.com', 'yellowpages.com', 'facebook.com', 'homeadvisor.com',
'thumbtack.com', 'angi.com', 'porch.com'
];
let hijackedBy: string | undefined;
const isDirectory = directoryDomains.some(d => {
if (firstResult.link?.includes(d)) {
hijackedBy = d;
return true;
}
return false;
});
if (isDirectory) {
const dirName = hijackedBy?.replace('.com', '').toUpperCase();
return {
found: false,
hijackedBy,
details: `🚨 IDENTITY THEFT: ${dirName} owns the #1 spot for your business name. Every time a customer searches for you, THEY get the click. They profit from YOUR reputation. This is the #1 reason small businesses lose customers to competitors.`
};
}
return {
found: true,
details: "Great! You control the #1 spot for your business name."
};
}
Scoring System
We calculate a visibility score out of 100:
function calculateResults(
mapsCheck, websiteCheck, rankingCheck, reputationCheck,
businessName, trade, location
): ScanResult {
let score = 0;
// Google Maps: 35 points max
if (mapsCheck.found) {
score += 20;
if (mapsCheck.claimed) score += 15;
}
// Website: 30 points max
if (websiteCheck.found) score += 30;
// Reputation: 15 points max
if (reputationCheck.found) score += 15;
// Search Ranking: 20 points max
if (rankingCheck.found) {
if (rankingCheck.position <= 3) score += 20;
else if (rankingCheck.position <= 5) score += 15;
else if (rankingCheck.position <= 10) score += 10;
}
// Determine status
let status: "invisible" | "weak" | "visible" | "strong";
if (score <= 25) status = "invisible";
else if (score <= 50) status = "weak";
else if (score <= 75) status = "visible";
else status = "strong";
// Estimate missed leads
const baseLeads = 60; // Typical well-optimized local business
const missedPercentage = (100 - score) / 100;
const estimatedMissedCalls = Math.round(baseLeads * missedPercentage);
return {
overallScore: score,
status,
estimatedMissedCalls,
checks: { mapsCheck, websiteCheck, rankingCheck, reputationCheck },
recommendations: generateRecommendations(...)
};
}
The Results Are Brutal
Typical contractor scan results:
- Score: 25/100 (Invisible)
- Google Maps: Found but unclaimed
- Website: None (only Facebook page)
- Search Ranking: Not in top 20
- Brand Control: Yelp owns the #1 spot
The message: “You’re losing an estimated 45 calls per month because customers can’t find you online.”
Tool 2: Profit Calculator
What It Does
Contractors enter their job numbers:
- Project quote
- Material costs
- Hours worked
- Crew size
We calculate their true hourly wage after:
- Overhead (insurance, vehicle, tools): ~20%
- Taxes: ~20%
Then AI delivers the brutal truth.
The Technical Implementation
export async function POST(request: NextRequest) {
const { projectQuote, materialCosts, hoursWorked, crewSize, trade, location, description } = await request.json();
// 1. Perform the hard math
const grossProfit = projectQuote - materialCosts;
const totalManHours = hoursWorked * crewSize;
const grossHourlyRate = totalManHours > 0 ? grossProfit / totalManHours : 0;
// Estimate overhead (insurance, vehicle, tools): ~20%
// Estimate taxes: ~20%
// Total deduction factor: 0.6 (retaining 60% of gross labor profit)
const netHourlyRate = grossHourlyRate * 0.6;
// 2. Generate AI analysis
const analysis = await generateAIAnalysis(
projectQuote, materialCosts, hoursWorked, crewSize,
grossProfit, grossHourlyRate, netHourlyRate,
trade, location, description
);
return NextResponse.json({
inputs: { projectQuote, materialCosts, hoursWorked, crewSize, trade, location },
metrics: { grossProfit, totalManHours, grossHourlyRate, netHourlyRate },
analysis
});
}
AI Analysis
The AI acts as a “tough, honest construction business coach”:
const systemPrompt = `You are a tough, honest construction business coach.
Your job is to analyze a contractor's project numbers and tell them the brutal truth about their profitability.
Be direct. Don't sugarcoat it. If they lost money or worked for cheap, say it.
Context:
- Trade: ${trade}
- Location: ${location || "Not specified"}
- Project Description: ${description || "Standard job"}
`;
const userPrompt = `
Here are the numbers for a recent job:
- Project Quote: $${projectQuote}
- Material Costs: $${materialCosts}
- Hours Worked: ${hoursWorked}
- Crew Size: ${crewSize}
Calculated Metrics:
- Gross Profit: $${grossProfit}
- Total Man-Hours: ${totalManHours}
- Gross Hourly Rate: $${grossHourlyRate.toFixed(2)}/hr
- Net Hourly Rate (after ~40% overhead/taxes): $${netHourlyRate.toFixed(2)}/hr
Analyze this. Return JSON:
{
"brutalTruth": "Short punchy headline",
"reasons": ["3 short bullet points"],
"recommendedPrice": number,
"targetHourlyRate": number,
"tip": "One actionable tip"
}`;
const analysis = await generateStructuredData<AIAnalysis>(
systemPrompt,
userPrompt,
{ temperature: 0.3 }
);
Example Output
Input:
- Quote: $5,000
- Materials: $2,000
- Hours: 40
- Crew: 2
Output:
{
"brutalTruth": "You worked for $18.75/hour. That's less than a Walmart cashier.",
"reasons": [
"Materials ate 40% of your quote—too high for this job type",
"80 man-hours at $37.50 gross means you're pricing labor at half market rate",
"After overhead and taxes, you netted less than minimum wage in California"
],
"recommendedPrice": 7500,
"targetHourlyRate": 75,
"tip": "Add a 50% markup to your labor estimate before quoting. Your time is worth more than this."
}
Why We Built These Tools
Strategy: Value-First Marketing
Traditional marketing: “Buy our product!” Our approach: “Here’s a free tool that shows you a painful truth. When you’re ready to fix it, we’re here.”
The funnel:
- Contractor uses Visibility Scan
- Sees they’re invisible online (score: 25/100)
- Sees Yelp is hijacking their brand
- Realizes they need a professional website
- WebZum is right there to help
The Profit Calculator:
- Contractor enters job numbers
- Discovers they’re working for $18/hour
- Realizes they need to raise prices
- Needs professional online presence to justify higher prices
- WebZum is right there to help
Why Free?
Cost to us: ~$0.01 per scan (Google API + AI) Value to contractor: Priceless insight Conversion rate: Much higher than cold ads
The math works: If 1 in 100 users converts to a $29/month subscription, we make $29 for $1 in API costs.
The Technical Challenges
Challenge 1: Rate Limiting
Problem: Google Search API has limits
Solution: Caching + parallel requests
// Run all checks in parallel
const [mapsCheck, websiteCheck, rankingCheck, reputationCheck] = await Promise.all([
checkGoogleMapsPresence(businessName, location),
checkWebsitePresence(businessName),
checkSearchRanking(businessName, trade, location),
checkReputation(businessName, location)
]);
Challenge 2: Accurate Trade Mapping
Problem: “Plumber” vs “Plumbing” vs “Plumbing Services”
Solution: Normalize trade names for search
function normalizeTradeForSearch(trade: string): string {
const tradeMap: Record<string, string> = {
'plumber': 'plumber',
'plumbing': 'plumber',
'electrician': 'electrician',
'electrical': 'electrician',
'hvac': 'hvac',
'heating and cooling': 'hvac',
// ... more mappings
};
const normalized = trade.toLowerCase().trim();
return tradeMap[normalized] || normalized;
}
Challenge 3: AI Consistency
Problem: AI sometimes gives inconsistent advice
Solution: Structured output with Zod validation
const AIAnalysisSchema = z.object({
brutalTruth: z.string().describe("Short punchy headline"),
reasons: z.array(z.string()).length(3),
recommendedPrice: z.number(),
targetHourlyRate: z.number(),
tip: z.string()
});
The Results
Visibility Scan usage: 500+ scans in first week Average score: 32/100 (most contractors are invisible) Top problem: Brand hijacking by directories (78% of scans)
Profit Calculator usage: 300+ calculations in first week Average net hourly rate: $24/hour (below target of $50-75) Top problem: Underpricing labor (65% of jobs)
Conversion to WebZum: 8% of tool users start website generation
Why This Matters for SaaS
Traditional SaaS marketing:
- Run ads → Landing page → Sign up → Hope they convert
Our approach:
- Free tool → Painful truth → Solution → Natural conversion
Key insights:
- Give value first: Free tools build trust
- Show the problem: Don’t tell them they need you—show them
- Make it personal: Their business name, their numbers
- Be brutally honest: Contractors respect directness
Try the tools yourself:
Building a SaaS? Key takeaway: Build free tools that expose painful truths. When users see the problem clearly, they’ll seek the solution. You just need to be there when they’re ready.
The best marketing doesn’t feel like marketing.