.do
Service TypesData Services

Data Enrichment Services

Build data enrichment services that augment existing data with external information and intelligence

Data enrichment services augment existing data with additional information from external sources, transforming basic records into comprehensive, actionable datasets. These services are essential for sales, marketing, and business intelligence operations.

Overview

Data enrichment takes incomplete or basic data and enhances it with relevant information from authoritative sources. Whether you're enriching contact records with social profiles, adding company information to leads, or enhancing addresses with geographic coordinates, enrichment services create more valuable and actionable data.

Key Capabilities

  • Contact Enrichment: Add email verification, phone numbers, job titles, social profiles
  • Company Enrichment: Add firmographic data, revenue, employee count, technology stack
  • Geographic Enrichment: Add coordinates, time zones, demographics, boundaries
  • Product Enrichment: Add descriptions, images, specifications, reviews
  • Lead Scoring: Calculate quality scores based on enriched attributes
  • Data Validation: Verify and correct existing data during enrichment

Common Use Cases

  1. Sales Lead Enrichment: Enhance leads with decision-maker contact info
  2. Marketing Personalization: Add demographic and behavioral data
  3. Risk Assessment: Enrich applications with credit and background data
  4. E-commerce: Enhance product catalogs with detailed information
  5. Customer Analytics: Add lifetime value predictions and segmentation
  6. Fraud Detection: Enrich transactions with risk signals

Building Your First Enrichment Service

Let's start with a contact enrichment service:

import $, { ai, db, on, send } from 'sdk.do'

const contactEnrichmentService = await $.Service.create({
  name: 'Contact Enricher',
  description: 'Enrich contact records with email, phone, social profiles, and company data',
  type: $.ServiceType.DataEnrichment,
  subtype: 'contact-enrichment',

  input: {
    required: ['contacts'],
    optional: ['enrichmentLevel', 'sources', 'validateExisting'],
  },

  output: {
    enrichedContacts: 'array',
    enrichmentRate: 'number',
    fieldsAdded: 'number',
    validationResults: 'object',
  },

  enrichmentLevels: {
    basic: ['email-verification'],
    standard: ['email-verification', 'company-info', 'job-title'],
    premium: ['email-verification', 'company-info', 'job-title', 'social-profiles', 'phone-number', 'education'],
  },

  pricing: {
    model: 'per-contact',
    levels: {
      basic: 0.25,
      standard: 0.75,
      premium: 1.5,
    },
    credits: true, // Only charge for successful enrichments
  },
})

on.ServiceRequest.created(async (request) => {
  if (request.serviceId !== contactEnrichmentService.id) return

  const { contacts, enrichmentLevel = 'standard', sources, validateExisting } = request.inputs
  const enrichmentFields = contactEnrichmentService.enrichmentLevels[enrichmentLevel]

  try {
    const results = []
    let totalFieldsAdded = 0
    let successfulEnrichments = 0

    for (const contact of contacts) {
      const enriched = { ...contact }
      const additions = {}
      const validations = {}

      // Email verification
      if (enrichmentFields.includes('email-verification') && contact.email) {
        const verification = await verifyEmail(contact.email)
        validations.email = verification
        if (!verification.valid) {
          additions.emailStatus = 'invalid'
          additions.emailReason = verification.reason
        } else {
          additions.emailStatus = 'valid'
          additions.emailDeliverability = verification.deliverability
        }
        totalFieldsAdded += 2
      }

      // Company information
      if (enrichmentFields.includes('company-info') && contact.company) {
        const companyData = await enrichCompanyData(contact.company, contact.website)
        if (companyData) {
          additions.companySize = companyData.employeeCount
          additions.companyRevenue = companyData.revenue
          additions.companyIndustry = companyData.industry
          additions.companyLocation = companyData.headquarters
          additions.companyFounded = companyData.foundedYear
          totalFieldsAdded += 5
          successfulEnrichments++
        }
      }

      // Job title normalization
      if (enrichmentFields.includes('job-title') && contact.jobTitle) {
        const normalized = await ai.generate({
          model: 'gpt-5',
          prompt: `Normalize this job title to a standard format: "${contact.jobTitle}"

          Provide:
          1. Normalized title
          2. Seniority level (entry, mid, senior, executive)
          3. Department (sales, marketing, engineering, etc.)
          4. Decision-making authority (low, medium, high)`,
          temperature: 0.1,
        })

        const parsed = parseJobTitleAnalysis(normalized.content)
        additions.jobTitleNormalized = parsed.title
        additions.seniorityLevel = parsed.seniority
        additions.department = parsed.department
        additions.decisionAuthority = parsed.authority
        totalFieldsAdded += 4
      }

      // Social profiles
      if (enrichmentFields.includes('social-profiles')) {
        const socialProfiles = await findSocialProfiles(contact)
        if (socialProfiles.linkedin) {
          additions.linkedinUrl = socialProfiles.linkedin
          additions.linkedinConnections = socialProfiles.linkedinData?.connections
          totalFieldsAdded += 2
        }
        if (socialProfiles.twitter) {
          additions.twitterHandle = socialProfiles.twitter
          additions.twitterFollowers = socialProfiles.twitterData?.followers
          totalFieldsAdded += 2
        }
      }

      // Phone number
      if (enrichmentFields.includes('phone-number') && !contact.phone) {
        const phoneData = await findPhoneNumber(contact)
        if (phoneData) {
          additions.phone = phoneData.number
          additions.phoneType = phoneData.type
          additions.phoneValid = phoneData.valid
          totalFieldsAdded += 3
          successfulEnrichments++
        }
      }

      // Education
      if (enrichmentFields.includes('education')) {
        const education = await findEducation(contact)
        if (education) {
          additions.education = education.schools
          additions.highestDegree = education.highestDegree
          totalFieldsAdded += 2
        }
      }

      // Merge enriched data
      Object.assign(enriched, additions)

      results.push({
        original: contact,
        enriched,
        fieldsAdded: Object.keys(additions).length,
        validations,
      })

      if (Object.keys(additions).length > 0) {
        successfulEnrichments++
      }
    }

    // Calculate enrichment rate
    const enrichmentRate = (successfulEnrichments / contacts.length) * 100

    // Deliver results
    await send.ServiceResult.deliver({
      requestId: request.id,
      outputs: {
        enrichedContacts: results.map((r) => r.enriched),
        enrichmentRate,
        fieldsAdded: totalFieldsAdded,
        validationResults: results.map((r) => r.validations),
        summary: {
          total: contacts.length,
          enriched: successfulEnrichments,
          failed: contacts.length - successfulEnrichments,
        },
      },
    })

    // Charge only for successful enrichments
    const pricePerContact = contactEnrichmentService.pricing.levels[enrichmentLevel]
    const cost = successfulEnrichments * pricePerContact

    await send.Payment.charge({
      customerId: request.customerId,
      amount: cost,
      description: `Contact enrichment (${successfulEnrichments} contacts, ${enrichmentLevel} level)`,
    })
  } catch (error) {
    await send.ServiceRequest.fail({
      requestId: request.id,
      error: error.message,
      retryable: true,
    })
  }
})

async function verifyEmail(email: string): Promise<any> {
  // Email verification logic
  const syntax = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  if (!syntax) {
    return { valid: false, reason: 'invalid-syntax' }
  }

  // Check MX records, SMTP, etc.
  const mx = await checkMXRecords(email)
  const smtp = await checkSMTP(email)

  return {
    valid: mx && smtp,
    deliverability: smtp ? 'high' : 'low',
    reason: !mx ? 'no-mx-records' : !smtp ? 'mailbox-not-found' : null,
  }
}

async function enrichCompanyData(companyName: string, website?: string): Promise<any> {
  // Lookup company data from multiple sources
  const sources = [lookupClearbit(companyName, website), lookupCrunchbase(companyName), lookupLinkedIn(companyName)]

  const results = await Promise.allSettled(sources)
  const successful = results.filter((r) => r.status === 'fulfilled').map((r) => r.value)

  if (successful.length === 0) return null

  // Merge data from multiple sources
  return mergeCompanyData(successful)
}

Lead Scoring Service

Calculate quality scores based on enriched data:

const leadScoringService = await $.Service.create({
  name: 'AI Lead Scorer',
  description: 'Score and prioritize leads based on enriched data and behavior',
  type: $.ServiceType.DataEnrichment,
  subtype: 'lead-scoring',

  input: {
    required: ['leads'],
    optional: ['scoringModel', 'weights', 'threshold'],
  },

  features: ['demographic-scoring', 'firmographic-scoring', 'behavioral-scoring', 'predictive-scoring', 'fit-score', 'engagement-score'],

  pricing: {
    model: 'per-lead',
    rate: 0.1,
    minimumCharge: 5.0,
  },
})

on.ServiceRequest.created(async (request) => {
  if (request.serviceId !== leadScoringService.id) return

  const { leads, scoringModel = 'standard', weights, threshold = 50 } = request.inputs

  try {
    const scoredLeads = []

    for (const lead of leads) {
      // First, enrich the lead with additional data
      const enriched = await enrichLeadData(lead)

      // Calculate demographic score (0-100)
      const demographicScore = calculateDemographicScore(enriched, weights?.demographic)

      // Calculate firmographic score (0-100)
      const firmographicScore = calculateFirmographicScore(enriched, weights?.firmographic)

      // Calculate behavioral score (0-100)
      const behavioralScore = calculateBehavioralScore(enriched, weights?.behavioral)

      // Use AI for predictive scoring
      const predictiveScore = await ai.predict({
        model: 'gpt-5',
        type: 'lead-conversion-probability',
        features: {
          ...enriched,
          demographicScore,
          firmographicScore,
          behavioralScore,
        },
        historicalData: await getHistoricalConversions(),
      })

      // Calculate composite score
      const compositeScore = calculateCompositeScore(
        {
          demographic: demographicScore,
          firmographic: firmographicScore,
          behavioral: behavioralScore,
          predictive: predictiveScore.probability * 100,
        },
        weights
      )

      // Classify lead
      const classification = classifyLead(compositeScore, threshold)

      scoredLeads.push({
        ...enriched,
        scores: {
          composite: compositeScore,
          demographic: demographicScore,
          firmographic: firmographicScore,
          behavioral: behavioralScore,
          predictive: predictiveScore.probability * 100,
        },
        classification,
        conversionProbability: predictiveScore.probability,
        recommendedActions: predictiveScore.recommendations,
      })
    }

    // Sort by score
    scoredLeads.sort((a, b) => b.scores.composite - a.scores.composite)

    // Deliver results
    await send.ServiceResult.deliver({
      requestId: request.id,
      outputs: {
        leads: scoredLeads,
        summary: {
          total: leads.length,
          hot: scoredLeads.filter((l) => l.classification === 'hot').length,
          warm: scoredLeads.filter((l) => l.classification === 'warm').length,
          cold: scoredLeads.filter((l) => l.classification === 'cold').length,
          averageScore: scoredLeads.reduce((sum, l) => sum + l.scores.composite, 0) / scoredLeads.length,
        },
      },
    })

    // Charge for scoring
    const cost = Math.max(leads.length * leadScoringService.pricing.rate, leadScoringService.pricing.minimumCharge)

    await send.Payment.charge({
      customerId: request.customerId,
      amount: cost,
      description: `Lead scoring (${leads.length} leads)`,
    })
  } catch (error) {
    await send.ServiceRequest.fail({
      requestId: request.id,
      error: error.message,
      retryable: true,
    })
  }
})

function calculateDemographicScore(lead: any, weights?: any): number {
  let score = 0
  const w = weights || { jobTitle: 30, seniority: 25, department: 20, email: 15, phone: 10 }

  // Job title scoring
  if (lead.jobTitleNormalized) {
    const titleScore = scoreJobTitle(lead.jobTitleNormalized)
    score += (titleScore / 100) * w.jobTitle
  }

  // Seniority scoring
  if (lead.seniorityLevel) {
    const seniorityMap = { executive: 100, senior: 75, mid: 50, entry: 25 }
    score += (seniorityMap[lead.seniorityLevel] || 0) * (w.seniority / 100)
  }

  // Department scoring
  if (lead.department) {
    const departmentMap = { executive: 100, sales: 80, marketing: 70, it: 60 }
    score += (departmentMap[lead.department] || 50) * (w.department / 100)
  }

  // Contact info completeness
  if (lead.emailStatus === 'valid') score += w.email
  if (lead.phoneValid) score += w.phone

  return Math.min(score, 100)
}

function calculateFirmographicScore(lead: any, weights?: any): number {
  let score = 0
  const w = weights || { companySize: 30, revenue: 25, industry: 20, location: 15, technology: 10 }

  // Company size scoring
  if (lead.companySize) {
    const sizeScore = scoreCompanySize(lead.companySize)
    score += (sizeScore / 100) * w.companySize
  }

  // Revenue scoring
  if (lead.companyRevenue) {
    const revenueScore = scoreRevenue(lead.companyRevenue)
    score += (revenueScore / 100) * w.revenue
  }

  // Industry match scoring
  if (lead.companyIndustry) {
    const industryScore = scoreIndustry(lead.companyIndustry)
    score += (industryScore / 100) * w.industry
  }

  // Location scoring
  if (lead.companyLocation) {
    const locationScore = scoreLocation(lead.companyLocation)
    score += (locationScore / 100) * w.location
  }

  return Math.min(score, 100)
}

Geographic Enrichment Service

Add location-based data to records:

const geoEnrichmentService = await $.Service.create({
  name: 'Geographic Enricher',
  description: 'Enrich addresses with coordinates, time zones, and demographic data',
  type: $.ServiceType.DataEnrichment,
  subtype: 'geo-enrichment',

  input: {
    required: ['records'],
    optional: ['addressField', 'enrichmentFields'],
  },

  enrichmentFields: ['coordinates', 'timezone', 'country-code', 'postal-code', 'census-data', 'demographics', 'weather', 'nearby-places'],

  pricing: {
    model: 'per-geocode',
    rate: 0.01,
    additionalFields: 0.005, // Per additional field
  },
})

on.ServiceRequest.created(async (request) => {
  if (request.serviceId !== geoEnrichmentService.id) return

  const { records, addressField = 'address', enrichmentFields = ['coordinates', 'timezone'] } = request.inputs

  try {
    const enrichedRecords = []
    let totalCost = 0

    for (const record of records) {
      const address = record[addressField]
      if (!address) {
        enrichedRecords.push(record)
        continue
      }

      const enriched = { ...record }

      // Geocode address
      const geocoded = await geocodeAddress(address)
      if (!geocoded) {
        enrichedRecords.push(record)
        continue
      }

      // Add base geocoding (always included)
      enriched.latitude = geocoded.lat
      enriched.longitude = geocoded.lng
      enriched.formattedAddress = geocoded.formattedAddress
      totalCost += geoEnrichmentService.pricing.rate

      // Add optional fields
      for (const field of enrichmentFields) {
        switch (field) {
          case 'timezone':
            enriched.timezone = await getTimezone(geocoded.lat, geocoded.lng)
            totalCost += geoEnrichmentService.pricing.additionalFields
            break

          case 'country-code':
            enriched.countryCode = geocoded.countryCode
            enriched.countryName = geocoded.countryName
            totalCost += geoEnrichmentService.pricing.additionalFields
            break

          case 'postal-code':
            enriched.postalCode = geocoded.postalCode
            totalCost += geoEnrichmentService.pricing.additionalFields
            break

          case 'census-data':
            enriched.censusData = await getCensusData(geocoded.lat, geocoded.lng)
            totalCost += geoEnrichmentService.pricing.additionalFields
            break

          case 'demographics':
            enriched.demographics = await getDemographics(geocoded.lat, geocoded.lng)
            totalCost += geoEnrichmentService.pricing.additionalFields
            break

          case 'weather':
            enriched.weather = await getWeather(geocoded.lat, geocoded.lng)
            totalCost += geoEnrichmentService.pricing.additionalFields
            break

          case 'nearby-places':
            enriched.nearbyPlaces = await getNearbyPlaces(geocoded.lat, geocoded.lng)
            totalCost += geoEnrichmentService.pricing.additionalFields
            break
        }
      }

      enrichedRecords.push(enriched)
    }

    // Deliver results
    await send.ServiceResult.deliver({
      requestId: request.id,
      outputs: {
        records: enrichedRecords,
        geocodedCount: enrichedRecords.filter((r) => r.latitude).length,
      },
    })

    // Charge for enrichment
    await send.Payment.charge({
      customerId: request.customerId,
      amount: totalCost,
      description: `Geographic enrichment (${enrichedRecords.length} records)`,
    })
  } catch (error) {
    await send.ServiceRequest.fail({
      requestId: request.id,
      error: error.message,
      retryable: true,
    })
  }
})

async function geocodeAddress(address: string): Promise<any> {
  // Call geocoding API (Google Maps, Mapbox, etc.)
  try {
    const response = await fetch(
      `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${process.env.GOOGLE_MAPS_API_KEY}`
    )

    const data = await response.json()

    if (data.results && data.results.length > 0) {
      const result = data.results[0]
      return {
        lat: result.geometry.location.lat,
        lng: result.geometry.location.lng,
        formattedAddress: result.formatted_address,
        countryCode: extractCountryCode(result),
        countryName: extractCountryName(result),
        postalCode: extractPostalCode(result),
      }
    }

    return null
  } catch (error) {
    return null
  }
}

Product Enrichment Service

Enhance product catalogs with detailed information:

const productEnrichmentService = await $.Service.create({
  name: 'Product Data Enricher',
  description: 'Enrich product catalogs with descriptions, images, specs, and reviews',
  type: $.ServiceType.DataEnrichment,
  subtype: 'product-enrichment',

  input: {
    required: ['products'],
    optional: ['enrichmentFields', 'sources', 'language'],
  },

  enrichmentFields: ['description', 'images', 'specifications', 'reviews', 'pricing', 'availability', 'alternatives'],

  pricing: {
    model: 'per-product',
    base: 0.5,
    fields: {
      description: 0.1,
      images: 0.15,
      specifications: 0.2,
      reviews: 0.1,
      pricing: 0.05,
    },
  },
})

on.ServiceRequest.created(async (request) => {
  if (request.serviceId !== productEnrichmentService.id) return

  const { products, enrichmentFields, language = 'en' } = request.inputs

  try {
    const enrichedProducts = []

    for (const product of products) {
      const enriched = { ...product }
      let productCost = productEnrichmentService.pricing.base

      // Generate/enhance description
      if (enrichmentFields.includes('description') && !product.description) {
        enriched.description = await ai.generate({
          model: 'gpt-5',
          prompt: `Write a compelling product description for:

          Product: ${product.name}
          Category: ${product.category}
          Features: ${product.features?.join(', ')}

          The description should be 2-3 paragraphs, highlighting benefits and use cases.`,
          temperature: 0.7,
        })
        productCost += productEnrichmentService.pricing.fields.description
      }

      // Find or generate images
      if (enrichmentFields.includes('images')) {
        const existingImages = product.images || []
        if (existingImages.length < 3) {
          const additionalImages = await findProductImages(product)
          enriched.images = [...existingImages, ...additionalImages]
          productCost += productEnrichmentService.pricing.fields.images
        }
      }

      // Gather specifications
      if (enrichmentFields.includes('specifications')) {
        const specs = await gatherProductSpecifications(product)
        enriched.specifications = {
          ...product.specifications,
          ...specs,
        }
        productCost += productEnrichmentService.pricing.fields.specifications
      }

      // Aggregate reviews
      if (enrichmentFields.includes('reviews')) {
        const reviews = await aggregateProductReviews(product)
        enriched.reviews = reviews
        enriched.averageRating = calculateAverageRating(reviews)
        enriched.reviewCount = reviews.length
        productCost += productEnrichmentService.pricing.fields.reviews
      }

      // Check competitive pricing
      if (enrichmentFields.includes('pricing')) {
        const priceComparison = await compareProductPricing(product)
        enriched.priceComparison = priceComparison
        enriched.priceCompetitiveness = analyzePriceCompetitiveness(product.price, priceComparison)
        productCost += productEnrichmentService.pricing.fields.pricing
      }

      // Find alternatives
      if (enrichmentFields.includes('alternatives')) {
        enriched.alternatives = await findAlternativeProducts(product)
      }

      enrichedProducts.push(enriched)
    }

    // Deliver results
    await send.ServiceResult.deliver({
      requestId: request.id,
      outputs: {
        products: enrichedProducts,
        summary: {
          total: products.length,
          fieldsEnriched: enrichmentFields,
        },
      },
    })

    // Calculate total cost
    const totalCost = products.length * productCost

    await send.Payment.charge({
      customerId: request.customerId,
      amount: totalCost,
      description: `Product enrichment (${products.length} products)`,
    })
  } catch (error) {
    await send.ServiceRequest.fail({
      requestId: request.id,
      error: error.message,
      retryable: true,
    })
  }
})

Real-Time Enrichment Service

Enrich data in real-time as it flows through your system:

const realtimeEnrichmentService = await $.Service.create({
  name: 'Real-Time Data Enricher',
  description: 'Enrich data streams in real-time with sub-100ms latency',
  type: $.ServiceType.DataEnrichment,
  subtype: 'real-time',

  latency: '<100ms',
  throughput: '10000 records/second',

  pricing: {
    model: 'subscription',
    tiers: [
      { name: 'starter', price: 200, throughput: '1000 records/sec', enrichments: ['basic'] },
      { name: 'professional', price: 1000, throughput: '5000 records/sec', enrichments: ['basic', 'standard'] },
      { name: 'enterprise', price: 5000, throughput: '25000 records/sec', enrichments: ['basic', 'standard', 'premium'] },
    ],
  },
})

on.Stream.data(async (stream) => {
  const enrichmentConfig = await db.EnrichmentConfig.get({
    where: { streamId: stream.id },
  })

  if (!enrichmentConfig) return

  // Create enrichment cache for performance
  const cache = new Map()

  for await (const record of stream) {
    try {
      const enriched = { ...record }

      // Apply enrichments based on config
      for (const enrichment of enrichmentConfig.enrichments) {
        const cacheKey = `${enrichment.type}:${record[enrichment.keyField]}`

        // Check cache first
        if (cache.has(cacheKey)) {
          Object.assign(enriched, cache.get(cacheKey))
          continue
        }

        // Perform enrichment
        const enrichmentData = await performEnrichment(record, enrichment)

        if (enrichmentData) {
          Object.assign(enriched, enrichmentData)
          // Cache for 5 minutes
          cache.set(cacheKey, enrichmentData)
          setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000)
        }
      }

      // Emit enriched record
      await send.Stream.emit({
        streamId: stream.id,
        data: enriched,
        latency: Date.now() - record.timestamp,
      })
    } catch (error) {
      // Emit original record on error
      await send.Stream.emit({
        streamId: stream.id,
        data: record,
        enrichmentError: error.message,
      })
    }
  }
})

async function performEnrichment(record: any, config: any): Promise<any> {
  switch (config.type) {
    case 'ip-geolocation':
      return await enrichIPGeolocation(record[config.keyField])

    case 'user-agent-parsing':
      return await parseUserAgent(record[config.keyField])

    case 'company-lookup':
      return await lookupCompany(record[config.keyField])

    case 'email-domain':
      return await enrichEmailDomain(record[config.keyField])

    case 'phone-carrier':
      return await lookupPhoneCarrier(record[config.keyField])

    default:
      return null
  }
}

Batch Enrichment Service

Enrich large datasets efficiently:

const batchEnrichmentService = await $.Service.create({
  name: 'Batch Data Enricher',
  description: 'Efficiently enrich large datasets with optimized batch processing',
  type: $.ServiceType.DataEnrichment,
  subtype: 'batch',

  maxBatchSize: 100000,
  averageProcessingTime: '1000 records/minute',

  pricing: {
    model: 'per-thousand',
    rate: 10.0, // $10 per 1000 records
    volume: [
      { min: 0, max: 10000, rate: 10.0 },
      { min: 10001, max: 100000, rate: 7.5 },
      { min: 100001, max: Infinity, rate: 5.0 },
    ],
  },
})

on.ServiceRequest.created(async (request) => {
  if (request.serviceId !== batchEnrichmentService.id) return

  const { dataUrl, enrichmentConfig, outputFormat = 'json' } = request.inputs

  try {
    // Download dataset
    await send.ServiceProgress.updated({
      requestId: request.id,
      stage: 'downloading',
      progress: 0,
    })

    const dataset = await downloadDataset(dataUrl)

    // Process in batches
    const batchSize = 1000
    const enrichedData = []

    for (let i = 0; i < dataset.length; i += batchSize) {
      const batch = dataset.slice(i, i + batchSize)

      // Enrich batch in parallel
      const enrichedBatch = await Promise.all(
        batch.map(async (record) => {
          const enriched = { ...record }

          for (const enrichment of enrichmentConfig) {
            const data = await getEnrichmentData(record, enrichment)
            if (data) Object.assign(enriched, data)
          }

          return enriched
        })
      )

      enrichedData.push(...enrichedBatch)

      // Update progress
      await send.ServiceProgress.updated({
        requestId: request.id,
        stage: 'enriching',
        progress: (i + batchSize) / dataset.length,
        message: `Enriched ${Math.min(i + batchSize, dataset.length)} of ${dataset.length} records`,
      })
    }

    // Upload result
    const resultUrl = await uploadEnrichedDataset(enrichedData, outputFormat)

    // Deliver results
    await send.ServiceResult.deliver({
      requestId: request.id,
      outputs: {
        url: resultUrl,
        recordCount: enrichedData.length,
        enrichmentRate: calculateEnrichmentRate(dataset, enrichedData),
      },
    })

    // Calculate cost
    const thousands = Math.ceil(dataset.length / 1000)
    const rate = getVolumeRate(dataset.length, batchEnrichmentService.pricing.volume)
    const cost = thousands * rate

    await send.Payment.charge({
      customerId: request.customerId,
      amount: cost,
      description: `Batch enrichment (${dataset.length} records)`,
    })
  } catch (error) {
    await send.ServiceRequest.fail({
      requestId: request.id,
      error: error.message,
      retryable: true,
    })
  }
})

Pricing Models for Enrichment Services

Per-Record Pricing

Best for: Contact and lead enrichment

pricing: {
  model: 'per-record',
  rate: 0.50,
  creditsOnly: true, // Only charge for successful enrichments
}

Tiered Pricing

Best for: Different enrichment levels

pricing: {
  model: 'tiered',
  levels: {
    basic: 0.25,
    standard: 0.75,
    premium: 1.50,
  },
}

Subscription Pricing

Best for: Real-time and high-volume enrichment

pricing: {
  model: 'subscription',
  tiers: [
    { name: 'starter', price: 200, records: 10000 },
    { name: 'professional', price: 1000, records: 100000 },
    { name: 'enterprise', price: 5000, records: 1000000 },
  ],
}

Credit-Based Pricing

Best for: Variable enrichment needs

pricing: {
  model: 'credits',
  creditCost: 1.0, // $1 = 100 credits
  enrichmentCosts: {
    emailVerification: 1,
    phoneNumber: 5,
    socialProfiles: 10,
    companyData: 15,
  },
}

Best Practices

1. Cache Enrichment Results

Avoid redundant API calls:

const enrichmentCache = new Map()

async function getCachedEnrichment(key: string, enrichFn: () => Promise<any>, ttl = 3600000): Promise<any> {
  if (enrichmentCache.has(key)) {
    const cached = enrichmentCache.get(key)
    if (Date.now() - cached.timestamp < ttl) {
      return cached.data
    }
  }

  const data = await enrichFn()
  enrichmentCache.set(key, { data, timestamp: Date.now() })
  return data
}

2. Handle API Rate Limits

Implement retry logic:

async function enrichWithRetry(fn: () => Promise<any>, maxRetries = 3): Promise<any> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      if (error.status === 429 && i < maxRetries - 1) {
        // Rate limited, wait and retry
        await sleep(Math.pow(2, i) * 1000)
        continue
      }
      throw error
    }
  }
}

3. Validate Enriched Data

Ensure quality:

function validateEnrichment(original: any, enriched: any, config: any): boolean {
  // Check that enrichment added expected fields
  for (const field of config.requiredFields) {
    if (!enriched[field]) return false
  }

  // Check data quality
  for (const [field, validator] of Object.entries(config.validators)) {
    if (enriched[field] && !validator(enriched[field])) {
      return false
    }
  }

  return true
}

Next Steps