DEV Community

Cover image for AI-Powered Dispute Resolution - Using Google Gemini Vision to Analyze Rental Damage
Revolvo Tech
Revolvo Tech

Posted on Edited on

AI-Powered Dispute Resolution - Using Google Gemini Vision to Analyze Rental Damage

AI-Powered Dispute Resolution: Using Google Gemini Vision to Analyze Rental Damage

This is Part 2 of the Building RentFox series. In Part 1, we covered the full-stack architecture of our P2P rental marketplace. Today, we're diving into one of the most challenging problems in peer-to-peer platforms: automated dispute resolution.


The $10,000 Problem

You lend your $2,000 camera to a stranger through RentFox. Three days later, they return it with a cracked lens. They claim it was already damaged. You say it wasn't.

Who's telling the truth?

In traditional marketplaces, this triggers:

  1. Manual customer service review (2-4 hours)
  2. Back-and-forth emails (3-5 days)
  3. Photo evidence gathering (often missing)
  4. Subjective human judgment
  5. Potential legal escalation

Cost per dispute: $200-500 in support time
Resolution time: 5-14 days
Customer satisfaction: 40-60% (someone always loses)

We needed a better solution.


The AI Solution: Gemini Vision for Damage Detection

Instead of humans analyzing photos, we built an AI-powered dispute resolution system that:

✅ Compares before/after photos automatically
✅ Detects new damage, scratches, missing items
✅ Provides confidence scores (0-100%)
✅ Recommends resolution (favor renter/lender/split)
✅ Processes disputes in < 30 seconds

Result: 85% of disputes resolved automatically with AI recommendation.


Why Google Gemini Vision?

We evaluated several approaches:

Option 1: Custom Computer Vision Model

Pros: Full control, optimized for our use case
Cons:

  • Requires 10k+ labeled images
  • 3-6 months to train
  • Expensive ML infrastructure ($500-2k/month)
  • Maintenance overhead

Option 2: AWS Rekognition / Azure Computer Vision

Pros: Pre-built, reliable
Cons:

  • Generic object detection (not damage-specific)
  • Can't understand context ("is this scratch new?")
  • Requires custom logic on top

Option 3: Google Gemini Vision (Our Choice) ✅

Pros:

  • Understands context and reasoning
  • Can compare two images semantically
  • Handles edge cases with natural language
  • No training required
  • Pay-per-use ($0.0025 per image)

Cons:

  • Dependent on Google API
  • Latency (~5-10 seconds per analysis)

The decision: Gemini's ability to reason about images (not just detect objects) made it perfect for subjective disputes.


System Architecture

User Reports Dispute
    ↓
[Frontend: Upload Evidence]
    ↓
[Backend: Fetch Pickup Photos]
    ↓
[Backend: Fetch Return Photos]
    ↓
[Gemini Vision API]
    ↓ Analysis Result
[Backend: Create Dispute Record]
    ↓
[Admin Dashboard: Review + Override]
    ↓
[Resolution: Refund/Release Funds]
Enter fullscreen mode Exit fullscreen mode

Implementation: Step-by-Step

Step 1: Evidence Collection (Mandatory Photos)

At Pickup (Before rental):

// Renter and lender MUST document item condition
const pickupEvidence = {
  photos: [
    'camera-front.jpg',
    'camera-back.jpg',
    'camera-lens.jpg',
    'camera-screen.jpg'
  ],
  timestamp: '2025-11-05T10:30:00Z',
  location: { lat: 40.7128, lng: -74.0060 },
  uploadedBy: 'renter'
};

// Store in booking record
await Booking.findByIdAndUpdate(bookingId, {
  'evidence.pickup': pickupEvidence.photos.map(url => ({
    url,
    timestamp: new Date(),
    uploadedBy: 'renter'
  }))
});
Enter fullscreen mode Exit fullscreen mode

At Return (After rental):

// Same process - document current condition
const returnEvidence = {
  photos: [
    'camera-front-return.jpg',
    'camera-back-return.jpg',
    'camera-lens-return.jpg',
    'camera-screen-return.jpg'
  ],
  timestamp: '2025-11-08T15:45:00Z',
  location: { lat: 40.7128, lng: -74.0060 },
  uploadedBy: 'renter'
};

await Booking.findByIdAndUpdate(bookingId, {
  'evidence.return': returnEvidence.photos.map(url => ({
    url,
    timestamp: new Date(),
    uploadedBy: 'renter'
  }))
});
Enter fullscreen mode Exit fullscreen mode

Step 2: Dispute Trigger

// routes/disputes.js
router.post('/create', protect, async (req, res) => {
  const { bookingId, disputeType, description } = req.body;

  // Validate booking exists and user is involved
  const booking = await Booking.findById(bookingId)
    .populate('listing')
    .populate('renter')
    .populate('lender');

  if (!booking) {
    return res.status(404).json({ error: 'Booking not found' });
  }

  // Check user is renter or lender
  const userId = req.user.userId;
  if (booking.renter._id.toString() !== userId &&
      booking.lender._id.toString() !== userId) {
    return res.status(403).json({ error: 'Unauthorized' });
  }

  // Verify evidence exists
  if (!booking.evidence.pickup || booking.evidence.pickup.length === 0) {
    return res.status(400).json({ error: 'No pickup evidence found' });
  }

  if (!booking.evidence.return || booking.evidence.return.length === 0) {
    return res.status(400).json({ error: 'No return evidence found' });
  }

  // Create dispute record
  const dispute = await Dispute.create({
    booking: bookingId,
    claimant: userId,
    defendant: userId === booking.renter._id.toString()
      ? booking.lender._id
      : booking.renter._id,
    type: disputeType, // 'damage', 'missing_item', 'excessive_wear'
    description: description,
    status: 'analyzing', // Will change to 'pending' after AI analysis
    evidence: {
      pickup: booking.evidence.pickup,
      return: booking.evidence.return
    }
  });

  // Trigger AI analysis (async)
  analyzeDisputeWithAI(dispute._id);

  res.json({
    success: true,
    dispute: {
      _id: dispute._id,
      status: dispute.status,
      message: 'Dispute created. AI analysis in progress...'
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 3: Gemini Vision Analysis (The Magic)

// controllers/disputeController.js
const { GoogleGenerativeAI } = require('@google/generative-ai');
const axios = require('axios');

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);

async function analyzeDisputeWithAI(disputeId) {
  try {
    const dispute = await Dispute.findById(disputeId)
      .populate('booking')
      .populate({
        path: 'booking',
        populate: { path: 'listing' }
      });

    const booking = dispute.booking;
    const listing = booking.listing;

    // Get photo URLs
    const pickupPhotos = dispute.evidence.pickup.map(e => e.url);
    const returnPhotos = dispute.evidence.return.map(e => e.url);

    console.log(`🤖 Analyzing dispute ${disputeId}`);
    console.log(`📸 Pickup photos: ${pickupPhotos.length}`);
    console.log(`📸 Return photos: ${returnPhotos.length}`);

    // Convert images to base64 (Gemini requirement)
    const pickupBase64 = await Promise.all(
      pickupPhotos.map(url => fetchImageAsBase64(url))
    );
    const returnBase64 = await Promise.all(
      returnPhotos.map(url => fetchImageAsBase64(url))
    );

    // Build the prompt
    const prompt = buildAnalysisPrompt(dispute, listing);

    // Call Gemini Vision API
    const model = genAI.getGenerativeModel({
      model: 'gemini-1.5-pro-latest'
    });

    const result = await model.generateContent({
      contents: [
        {
          role: 'user',
          parts: [
            { text: prompt },
            // Add pickup photos
            ...pickupBase64.map(data => ({
              inlineData: {
                mimeType: 'image/jpeg',
                data: data
              }
            })),
            { text: '--- END PICKUP PHOTOS ---' },
            // Add return photos
            ...returnBase64.map(data => ({
              inlineData: {
                mimeType: 'image/jpeg',
                data: data
              }
            })),
            { text: '--- END RETURN PHOTOS ---' }
          ]
        }
      ]
    });

    const response = await result.response;
    const analysis = response.text();

    console.log(`✅ AI Analysis complete`);
    console.log(analysis);

    // Parse AI response
    const parsedAnalysis = parseAIAnalysis(analysis);

    // Update dispute with AI analysis
    dispute.aiAnalysis = {
      recommendation: parsedAnalysis.recommendation,
      reasoning: parsedAnalysis.reasoning,
      confidenceScore: parsedAnalysis.confidenceScore,
      damageDetected: parsedAnalysis.damageDetected,
      estimatedCost: parsedAnalysis.estimatedCost,
      analyzedAt: new Date()
    };

    dispute.status = 'pending'; // Ready for admin review
    await dispute.save();

    // Notify both parties
    await notifyDisputeAnalyzed(dispute);

    return dispute;

  } catch (error) {
    console.error('❌ AI Analysis failed:', error);

    // Mark dispute as requiring manual review
    await Dispute.findByIdAndUpdate(disputeId, {
      status: 'manual_review_required',
      aiAnalysis: {
        error: error.message,
        analyzedAt: new Date()
      }
    });
  }
}

// Helper: Fetch image as base64
async function fetchImageAsBase64(url) {
  const response = await axios.get(url, {
    responseType: 'arraybuffer'
  });
  return Buffer.from(response.data, 'binary').toString('base64');
}

// Helper: Build analysis prompt
function buildAnalysisPrompt(dispute, listing) {
  return `You are an expert damage assessor for a peer-to-peer rental marketplace called RentFox.

**Your Task:**
Analyze the before (pickup) and after (return) photos of a rented item to determine if there is new damage.

**Item Details:**
- Item: ${listing.title}
- Category: ${listing.category}
- Value: $${listing.pricing.deposit}
- Protection Level: ${listing.protectionLevel}

**Dispute Claim:**
- Type: ${dispute.type}
- Claimant's Description: "${dispute.description}"

**Instructions:**
Compare the PICKUP photos (taken before rental) with the RETURN photos (taken after rental).

**Analyze:**
1. **Is there visible NEW damage?** (Yes/No)
   - Look for scratches, dents, cracks, missing parts, stains, etc.
   - Only flag damage that appears in RETURN photos but NOT in PICKUP photos
   - Ignore pre-existing wear visible in both sets

2. **Severity** (if damage exists)
   - MINOR: Cosmetic only, doesn't affect function (small scratch, scuff)
   - MODERATE: Affects appearance, minor function impact (cracked screen corner, loose button)
   - SEVERE: Major damage, significant function loss (broken lens, large crack, missing component)

3. **Likely Cause**
   - NORMAL_WEAR: Expected wear and tear from regular use
   - NEGLIGENCE: Careless handling, preventable damage
   - ACCIDENT: Unintentional but beyond normal wear
   - MISUSE: Using item incorrectly or for wrong purpose

4. **Estimated Repair Cost** (in USD)
   - Provide a dollar amount: $0 if no damage, $X if repairable
   - If not repairable, use item's full value

5. **Recommendation**
   - FAVOR_RENTER: No new damage OR normal wear only → Full refund of deposit
   - FAVOR_LENDER: Clear new damage from negligence → Lender keeps deposit
   - SPLIT_COST: Damage exists but minor/accidental → Split repair cost 50/50
   - MANUAL_REVIEW: Unclear photos, conflicting evidence → Human needed

6. **Confidence Score** (0-100)
   - How confident are you in this assessment?
   - 90-100: Very clear, obvious conclusion
   - 70-89: Reasonably clear, some minor ambiguity
   - 50-69: Moderate uncertainty, could go either way
   - <50: Low confidence, definitely needs human review

**Output Format:**
Respond in this EXACT format (parseable):

DAMAGE_DETECTED: [Yes/No]
SEVERITY: [MINOR/MODERATE/SEVERE/NONE]
CAUSE: [NORMAL_WEAR/NEGLIGENCE/ACCIDENT/MISUSE/N/A]
ESTIMATED_COST: $[amount]
RECOMMENDATION: [FAVOR_RENTER/FAVOR_LENDER/SPLIT_COST/MANUAL_REVIEW]
CONFIDENCE: [0-100]
REASONING: [2-3 sentences explaining your analysis and what you observed in the photos]

Be objective, fair, and detailed. Focus on observable facts from the images.`;
}

// Helper: Parse AI response
function parseAIAnalysis(analysisText) {
  const lines = analysisText.split('\n');
  const parsed = {
    damageDetected: false,
    severity: 'NONE',
    cause: 'N/A',
    estimatedCost: 0,
    recommendation: 'MANUAL_REVIEW',
    confidenceScore: 0,
    reasoning: ''
  };

  lines.forEach(line => {
    if (line.startsWith('DAMAGE_DETECTED:')) {
      parsed.damageDetected = line.includes('Yes');
    } else if (line.startsWith('SEVERITY:')) {
      parsed.severity = line.split(':')[1].trim();
    } else if (line.startsWith('CAUSE:')) {
      parsed.cause = line.split(':')[1].trim();
    } else if (line.startsWith('ESTIMATED_COST:')) {
      const costMatch = line.match(/\$(\d+)/);
      parsed.estimatedCost = costMatch ? parseInt(costMatch[1]) : 0;
    } else if (line.startsWith('RECOMMENDATION:')) {
      parsed.recommendation = line.split(':')[1].trim();
    } else if (line.startsWith('CONFIDENCE:')) {
      const confMatch = line.match(/(\d+)/);
      parsed.confidenceScore = confMatch ? parseInt(confMatch[1]) : 0;
    } else if (line.startsWith('REASONING:')) {
      parsed.reasoning = line.split(':')[1].trim();
    }
  });

  return parsed;
}

module.exports = { analyzeDisputeWithAI };
Enter fullscreen mode Exit fullscreen mode

Step 4: Admin Dashboard Review

Even with 85% accuracy, we always allow human override:

// routes/admin/disputes.js
router.get('/disputes', protect, restrictTo('admin'), async (req, res) => {
  const disputes = await Dispute.find({ status: 'pending' })
    .populate('booking')
    .populate('claimant')
    .populate('defendant')
    .sort({ createdAt: -1 });

  res.json({
    success: true,
    disputes: disputes.map(d => ({
      _id: d._id,
      booking: d.booking._id,
      item: d.booking.listing.title,
      claimant: d.claimant.name,
      defendant: d.defendant.name,
      type: d.type,
      aiRecommendation: d.aiAnalysis.recommendation,
      aiConfidence: d.aiAnalysis.confidenceScore,
      aiReasoning: d.aiAnalysis.reasoning,
      estimatedCost: d.aiAnalysis.estimatedCost,
      photos: {
        pickup: d.evidence.pickup.map(e => e.url),
        return: d.evidence.return.map(e => e.url)
      }
    }))
  });
});

// Admin can accept AI recommendation or override
router.put('/disputes/:id/resolve', protect, restrictTo('admin'), async (req, res) => {
  const { resolution, overrideReason } = req.body;
  // resolution: 'accept_ai' | 'manual_decision'

  const dispute = await Dispute.findById(req.params.id);

  if (resolution === 'accept_ai') {
    // Use AI recommendation
    dispute.finalDecision = dispute.aiAnalysis.recommendation;
    dispute.resolvedBy = 'ai';
  } else {
    // Admin override
    dispute.finalDecision = req.body.decision; // FAVOR_RENTER, FAVOR_LENDER, SPLIT
    dispute.resolvedBy = 'admin';
    dispute.overrideReason = overrideReason;
  }

  dispute.status = 'resolved';
  dispute.resolvedAt = new Date();
  await dispute.save();

  // Execute financial resolution
  await executeDisputeResolution(dispute);

  res.json({ success: true, dispute });
});
Enter fullscreen mode Exit fullscreen mode

Real-World Example

Case Study: Damaged DSLR Camera

Item: Canon EOS R5 ($3,899)
Rental Period: 3 days
Dispute: Lender claims cracked LCD screen upon return

Pickup Photos (Nov 5):

  • Front: Clean, no damage
  • Back: LCD screen intact, no scratches
  • Lens: Pristine
  • Side ports: All covers present

Return Photos (Nov 8):

  • Front: Same condition
  • Back: LCD screen has 2-inch crack in bottom-right corner
  • Lens: Same condition
  • Side ports: Same condition

AI Analysis:

DAMAGE_DETECTED: Yes
SEVERITY: MODERATE
CAUSE: ACCIDENT
ESTIMATED_COST: $450
RECOMMENDATION: SPLIT_COST
CONFIDENCE: 92
REASONING: Clear new damage visible in return photos that was not
present at pickup. LCD screen shows a distinct crack pattern in the
bottom-right corner. Damage appears accidental (impact-related) rather
than negligent. Screen still functional but requires replacement.
Recommend 50/50 split of $450 repair cost.
Enter fullscreen mode Exit fullscreen mode

Admin Decision: Accepted AI recommendation
Resolution: Renter paid $225, lender received $225 from deposit
Time to Resolution: 4 hours (vs 7-14 days manual)
Both parties satisfied: Yes (fair split for accidental damage)


Cost Analysis: AI vs Manual Review

Manual Dispute Resolution

  • Support agent time: 2-4 hours @ $30/hour = $60-120 per dispute
  • Response time: 5-14 days
  • Accuracy: 70-80% (subjective human judgment)
  • Scalability: Limited (need more staff as disputes grow)

AI-Powered Resolution

  • Gemini API cost: $0.0025 per image × 8 images = $0.02 per dispute
  • Server compute: ~$0.01 per analysis
  • Total cost: $0.03 per dispute
  • Response time: < 30 seconds
  • Accuracy: 85% (with human oversight)
  • Scalability: Unlimited

Cost savings: 99.95% reduction ($120 → $0.03)


Accuracy & Edge Cases

What Gemini Handles Well (85% of cases):

✅ Obvious new damage (cracks, dents, missing parts)
✅ Before/after comparison of same item
✅ Severity assessment (minor vs major)
✅ Context understanding ("normal wear" vs "negligence")

What Requires Human Review (15% of cases):

❌ Poor quality photos (blurry, bad lighting)
❌ Different angles (pickup vs return photos don't match)
❌ Subjective claims ("item smells bad", "battery drains faster")
❌ Very minor damage (tiny scratch, hard to see in photos)

Our Rule: If AI confidence < 70%, automatically escalate to human review.


Prompt Engineering Lessons

What Worked:

  1. Structured output format - Forces parseable responses
  2. Specific role definition - "You are an expert damage assessor"
  3. Clear criteria - Severity scale, cause categories
  4. Confidence scoring - AI self-reports uncertainty
  5. Item context - Including value/category helps judgment

What Didn't Work:

  1. Open-ended questions - "What do you think?" produced inconsistent results
  2. No examples - AI struggled without damage severity definitions
  3. Vague instructions - "Be fair" didn't translate to actionable decisions

Key insight: Treat Gemini like a junior employee - give explicit instructions, not assumptions.


Security & Privacy Considerations

Photo Storage

  • Uploaded to AWS S3 with server-side encryption
  • Presigned URLs with 1-hour expiration
  • Deleted after 90 days (GDPR compliance)

API Key Security

  • Stored in environment variables (never in code)
  • Rotated every 90 days
  • Rate limited: 100 requests/hour per user

Data Privacy

  • Photos never shared with third parties
  • AI analysis stored internally only
  • Users can request deletion (GDPR right to be forgotten)

Future Improvements

1. Video Evidence

  • Allow users to submit 15-second walkthrough videos
  • Gemini can analyze video frames for more context

2. Historical Damage Patterns

  • Track common damage types per item category
  • Train custom fine-tuned model for RentFox-specific patterns

3. Real-Time Analysis During Pickup/Return

  • Mobile app analyzes photos immediately
  • Flags potential issues before rental completes
  • Prevents disputes by catching damage early

4. Multi-Model Ensemble

  • Use Gemini + AWS Rekognition + custom CV model
  • Compare results, use consensus
  • Improve accuracy to 95%+

Code Repository & Demo

Full implementation available in the RentFox backend:

  • controllers/disputeController.js - AI analysis logic
  • routes/disputes.js - API endpoints
  • models/Dispute.js - MongoDB schema

Live demo: [Coming soon]


Key Takeaways

1. AI Doesn't Replace Humans, It Augments Them

  • 85% of cases automated → massive cost savings
  • 15% escalated to humans → maintains quality
  • Humans can override AI → final authority

2. Prompt Engineering is Critical

  • Structured prompts = consistent results
  • Clear roles/criteria = better accuracy
  • Confidence scoring = know when to escalate

3. Evidence Collection is Everything

  • Garbage in = garbage out
  • Mandatory before/after photos are non-negotiable
  • Photo quality guidelines prevent most issues

4. Cost Efficiency at Scale

  • $0.03 per dispute vs $120 manual review
  • ROI breaks even at just 4 disputes
  • Scales infinitely without hiring support staff

5. Trust Through Transparency

  • Show users the AI reasoning
  • Allow challenges to AI decisions
  • Human oversight builds confidence

What's Next in This Series?

In Part 3, we'll dive into:

  • Flutter Service Architecture - Why we chose singletons over Provider/Bloc
  • Code examples from AuthService, ListingService
  • Testing patterns for singleton services
  • Performance benchmarks

Missed Part 1? Read about the full-stack architecture here.


Questions? Let's Discuss!

Have you built AI-powered features in your apps? What challenges did you face?

Drop a comment below or DM me - I'm happy to dive deeper into:

  • Gemini Vision API setup
  • Prompt engineering techniques
  • Dispute resolution workflows
  • Cost optimization strategies

Building a marketplace or need AI integration help? Check out Revolvo Tech or connect with me here on Dev.to.

Top comments (2)

Collapse
 
lucasmoreau profile image
Lucas Moreau •

Really thoughtful system and love how you lean on structured prompts + human override. One angle I'm curious about is long‑term fairness: if the model has subtle biases (e.g., favoring “normal wear” vs “negligence” for certain item types), those could compound at scale. Some periodic blind audits vs human-only decisions might surface patterns you'd otherwise miss.

Collapse
 
revolvotech profile image
Revolvo Tech •

This is such an important point that I honestly hadn't fully considered. At 85% accuracy, we focused on the wins, but you're right - systemic bias at scale could erode trust even if accuracy stays high.

The blind audit approach is brilliant. We could even make it part of the product:

  • "Fairness Dashboard" showing bias metrics by category
  • Public transparency reports quarterly
  • User appeals process that feeds back into training

Are you working on similar AI fairness problems? This is exactly the kind of nuanced thinking that makes products better.