krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

6c23daaaba2a954220ece2afa7193dbdcad55c22

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-22T18:47:37Z

feat(v3.2.0): add Data+ tier and external data integrations

- introduce Data+ tier for advanced data features
- add ownership history and DNS history
- expand subdomain discovery with external sources
- add domain pricing insights
- implement local usage/credit system
- integrate external data service layer with rate limiting
- extend export and CLI for Data+ features
 DomainDig/AppVersion.swift                |   4 +-
 DomainDig/ContentView.swift               | 438 +++++++++++++++++----
 DomainDig/DataAccessService.swift         |   2 +-
 DomainDig/DomainViewModel.swift           | 338 +++++++++++++++-
 DomainDig/ExternalDataService.swift       | 627 ++++++++++++++++++++++++++++++
 DomainDig/FeatureAccessService.swift      |  16 +-
 DomainDig/HistoryView.swift               |   3 +
 DomainDig/Models.swift                    | 158 +++++++-
 DomainDig/PaywallView.swift               |  13 +-
 DomainDig/PurchaseService.swift           |  36 +-
 DomainDig/RDAPService.swift               |   5 +
 DomainDig/SubdomainDiscoveryService.swift |   2 +-
 DomainDig/UsageCreditService.swift        |  73 ++++
 DomainDigCLI.swift                        | 208 +++++++++-
 DomainInspectionService.swift             |   8 +
 DomainReportBuilder.swift                 |  10 +-
 DomainReportExporter.swift                |  75 +++-
 LookupSnapshot.swift                      |  16 +
 18 files changed, 1935 insertions(+), 97 deletions(-)

diff --git a/DomainDig/AppVersion.swift b/DomainDig/AppVersion.swift
index d9f3fc7..f28a017 100644
--- a/DomainDig/AppVersion.swift
+++ b/DomainDig/AppVersion.swift
@@ -1,7 +1,7 @@
 import Foundation
 
 enum AppVersion {
-    static var current: String {
-        "3.1.0"
+    nonisolated static var current: String {
+        "3.2.0"
     }
 }
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index e6e7a41..a4e8db3 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -71,62 +71,11 @@ struct ContentView: View {
                                     .padding(.top, appDensity.metrics.cardSpacing)
                             }
                         }
-                        DomainSectionView(
-                            isCollapsed: sectionCollapsedBinding(.domain),
-                            rows: viewModel.domainRows,
-                            suggestions: viewModel.suggestionRows,
-                            showSuggestions: viewModel.availabilityResult?.status == .registered || viewModel.suggestionsLoading,
-                            availabilityLoading: viewModel.availabilityLoading,
-                            suggestionsLoading: viewModel.suggestionsLoading,
-                            provenance: viewModel.currentSnapshot.provenanceBySection[.availability],
-                            confidence: viewModel.currentSnapshot.availabilityConfidence,
-                            snapshotNote: viewModel.currentSnapshot.note,
-                            trackedDomain: viewModel.currentTrackedDomain,
-                            workflows: viewModel.currentDomainWorkflows,
-                            trackingLimitMessage: viewModel.trackingLimitMessage,
-                            onTrack: {
-                                _ = viewModel.trackCurrentDomain()
-                            },
-                            onTogglePinned: {
-                                guard let trackedDomain = viewModel.currentTrackedDomain else { return }
-                                viewModel.togglePinned(for: trackedDomain)
-                            },
-                            onEditNote: {
-                                guard let trackedDomain = viewModel.currentTrackedDomain else { return }
-                                trackingNoteDraft = trackedDomain.note ?? ""
-                                editingTrackedDomain = trackedDomain
-                            },
-                            onAddToWorkflow: {
-                                showingCurrentDomainWorkflowSheet = true
-                            },
-                            onOpenWorkflow: { workflow in
-                                navigationPath.append(WorkflowNavigationTarget(workflowID: workflow.id))
-                            },
-                            onRunWorkflow: { workflow in
-                                viewModel.rerunCurrentDomain(in: workflow)
-                            }
-                        )
+                        domainOverviewSection
                             .padding(.top, appDensity.metrics.sectionSpacing)
-                        OwnershipSectionView(
-                            isCollapsed: sectionCollapsedBinding(.ownership),
-                            rows: viewModel.ownershipRows,
-                            loading: viewModel.ownershipLoading,
-                            error: viewModel.ownershipError,
-                            provenance: viewModel.currentSnapshot.provenanceBySection[.ownership],
-                            confidence: viewModel.currentSnapshot.ownershipConfidence,
-                            showsHistoryPlaceholder: !DataAccessService.hasAccess(to: .ownershipHistory)
-                        )
+                        ownershipSection
                         .padding(.top, appDensity.metrics.sectionSpacing)
-                        SubdomainsSectionView(
-                            isCollapsed: sectionCollapsedBinding(.subdomains),
-                            rows: viewModel.subdomainRows,
-                            groups: viewModel.currentSubdomainGroups,
-                            loading: viewModel.subdomainsLoading,
-                            error: viewModel.subdomainsError,
-                            provenance: viewModel.currentSnapshot.provenanceBySection[.subdomains],
-                            confidence: viewModel.currentSnapshot.subdomainConfidence,
-                            showsExtendedPlaceholder: !DataAccessService.hasAccess(to: .extendedSubdomains)
-                        )
+                        subdomainsSection
                         .padding(.top, appDensity.metrics.sectionSpacing)
                         if !viewModel.currentDiffSections.isEmpty {
                             DomainDiffView(
@@ -137,17 +86,7 @@ struct ContentView: View {
                             )
                             .padding(.top, appDensity.metrics.sectionSpacing)
                         }
-                        DNSSectionView(
-                            isCollapsed: sectionCollapsedBinding(.dns),
-                            dnssecLabel: viewModel.dnssecLabel,
-                            patternSummary: viewModel.currentDNSPatterns,
-                            sections: viewModel.dnsRows,
-                            ptrMessage: viewModel.ptrMessage,
-                            loading: viewModel.dnsLoading || viewModel.ptrLoading,
-                            dnsProvenance: viewModel.currentSnapshot.provenanceBySection[.dns],
-                            ptrProvenance: viewModel.currentSnapshot.provenanceBySection[.ptr],
-                            sectionError: viewModel.dnsError
-                        )
+                        dnsSection
                         .padding(.top, appDensity.metrics.sectionSpacing)
                         WebSectionView(
                             isCollapsed: sectionCollapsedBinding(.web),
@@ -240,6 +179,9 @@ struct ContentView: View {
         .onAppear {
             domainFieldFocused = true
         }
+        .task {
+            await viewModel.refreshUsageCredits()
+        }
         .onChange(of: viewModel.searchedDomain) { _, _ in
             collapsedSections = defaultCollapsedSections
         }
@@ -372,6 +314,117 @@ struct ContentView: View {
         .padding(.vertical, appDensity.metrics.sectionSpacing)
     }
 
+    private var domainOverviewSection: some View {
+        let trackedDomain = viewModel.currentTrackedDomain
+        let workflows = viewModel.currentDomainWorkflows
+
+        return DomainSectionView(
+            isCollapsed: sectionCollapsedBinding(.domain),
+            rows: viewModel.domainRows,
+            suggestions: viewModel.suggestionRows,
+            showSuggestions: viewModel.availabilityResult?.status == .registered || viewModel.suggestionsLoading,
+            availabilityLoading: viewModel.availabilityLoading,
+            suggestionsLoading: viewModel.suggestionsLoading,
+            provenance: viewModel.currentSnapshot.provenanceBySection[.availability],
+            confidence: viewModel.currentSnapshot.availabilityConfidence,
+            snapshotNote: viewModel.currentSnapshot.note,
+            trackedDomain: trackedDomain,
+            workflows: workflows,
+            trackingLimitMessage: viewModel.trackingLimitMessage,
+            pricingLoading: viewModel.domainPricingLoading,
+            pricingError: viewModel.domainPricingError,
+            showsPricingPlaceholder: !DataAccessService.hasAccess(to: .domainPricing),
+            onTrack: {
+                _ = viewModel.trackCurrentDomain()
+            },
+            onTogglePinned: {
+                guard let trackedDomain else { return }
+                viewModel.togglePinned(for: trackedDomain)
+            },
+            onEditNote: {
+                guard let trackedDomain else { return }
+                trackingNoteDraft = trackedDomain.note ?? ""
+                editingTrackedDomain = trackedDomain
+            },
+            onAddToWorkflow: {
+                showingCurrentDomainWorkflowSheet = true
+            },
+            onOpenWorkflow: { workflow in
+                navigationPath.append(WorkflowNavigationTarget(workflowID: workflow.id))
+            },
+            onRunWorkflow: { workflow in
+                viewModel.rerunCurrentDomain(in: workflow)
+            }
+        )
+    }
+
+    private var ownershipSection: some View {
+        OwnershipSectionView(
+            isCollapsed: sectionCollapsedBinding(.ownership),
+            rows: viewModel.ownershipRows,
+            loading: viewModel.ownershipLoading,
+            error: viewModel.ownershipError,
+            provenance: viewModel.currentSnapshot.provenanceBySection[.ownership],
+            confidence: viewModel.currentSnapshot.ownershipConfidence,
+            showsHistoryPlaceholder: !DataAccessService.hasAccess(to: .ownershipHistory),
+            history: viewModel.ownershipHistory,
+            historyLoading: viewModel.ownershipHistoryLoading,
+            historyError: viewModel.ownershipHistoryError,
+            historyCreditStatus: viewModel.ownershipHistoryCreditStatus,
+            onLoadHistory: {
+                Task {
+                    await viewModel.loadOwnershipHistory()
+                }
+            }
+        )
+    }
+
+    private var subdomainsSection: some View {
+        SubdomainsSectionView(
+            isCollapsed: sectionCollapsedBinding(.subdomains),
+            rows: viewModel.subdomainRows,
+            groups: viewModel.currentSubdomainGroups,
+            loading: viewModel.subdomainsLoading,
+            error: viewModel.subdomainsError,
+            provenance: viewModel.currentSnapshot.provenanceBySection[.subdomains],
+            confidence: viewModel.currentSnapshot.subdomainConfidence,
+            showsExtendedPlaceholder: !DataAccessService.hasAccess(to: .extendedSubdomains),
+            extendedCount: viewModel.extendedSubdomains.count,
+            extendedLoading: viewModel.extendedSubdomainsLoading,
+            extendedError: viewModel.extendedSubdomainsError,
+            extendedCreditStatus: viewModel.extendedSubdomainsCreditStatus,
+            onLoadExtended: {
+                Task {
+                    await viewModel.loadExtendedSubdomains()
+                }
+            }
+        )
+    }
+
+    private var dnsSection: some View {
+        DNSSectionView(
+            isCollapsed: sectionCollapsedBinding(.dns),
+            dnssecLabel: viewModel.dnssecLabel,
+            patternSummary: viewModel.currentDNSPatterns,
+            sections: viewModel.dnsRows,
+            ptrMessage: viewModel.ptrMessage,
+            loading: viewModel.dnsLoading || viewModel.ptrLoading,
+            dnsProvenance: viewModel.currentSnapshot.provenanceBySection[.dns],
+            ptrProvenance: viewModel.currentSnapshot.provenanceBySection[.ptr],
+            sectionError: viewModel.dnsError,
+            history: viewModel.dnsHistory,
+            historyLoading: viewModel.dnsHistoryLoading,
+            historyError: viewModel.dnsHistoryError,
+            showsHistoryPlaceholder: !DataAccessService.hasAccess(to: .dnsHistory),
+            historyCreditStatus: viewModel.dnsHistoryCreditStatus,
+            onLoadHistory: {
+                Task {
+                    await viewModel.loadDNSHistory()
+                }
+            }
+        )
+    }
+
     private var actionButtons: some View {
         HStack {
             Spacer()
@@ -1192,6 +1245,9 @@ struct DomainSectionView: View {
     let trackedDomain: TrackedDomain?
     let workflows: [DomainWorkflow]
     let trackingLimitMessage: String?
+    let pricingLoading: Bool
+    let pricingError: String?
+    let showsPricingPlaceholder: Bool
     let onTrack: () -> Void
     let onTogglePinned: () -> Void
     let onEditNote: (() -> Void)?
@@ -1313,12 +1369,24 @@ struct DomainSectionView: View {
                         }
                     }
                 }
+                if pricingLoading {
+                    ProgressView("Loading external pricing…")
+                        .appLoadingStyle()
+                        .padding(.top, 4)
+                } else if let pricingError {
+                    MessageRowView(text: pricingError, isError: false)
+                        .padding(.top, 4)
+                } else if showsPricingPlaceholder {
+                    MessageRowView(text: "Pricing signals available in Data+", isError: false)
+                        .padding(.top, 4)
+                }
             }
         }
     }
 }
 
 struct OwnershipSectionView: View {
+    @Environment(\.appDensity) private var appDensity
     @Binding var isCollapsed: Bool
     let rows: [InfoRowViewData]
     let loading: Bool
@@ -1326,6 +1394,39 @@ struct OwnershipSectionView: View {
     let provenance: SectionProvenance?
     let confidence: ConfidenceLevel?
     let showsHistoryPlaceholder: Bool
+    let history: [DomainOwnershipHistoryEvent]
+    let historyLoading: Bool
+    let historyError: String?
+    let historyCreditStatus: UsageCreditStatus?
+    let onLoadHistory: (() -> Void)?
+
+    init(
+        isCollapsed: Binding<Bool>,
+        rows: [InfoRowViewData],
+        loading: Bool,
+        error: String?,
+        provenance: SectionProvenance?,
+        confidence: ConfidenceLevel?,
+        showsHistoryPlaceholder: Bool,
+        history: [DomainOwnershipHistoryEvent] = [],
+        historyLoading: Bool = false,
+        historyError: String? = nil,
+        historyCreditStatus: UsageCreditStatus? = nil,
+        onLoadHistory: (() -> Void)? = nil
+    ) {
+        _isCollapsed = isCollapsed
+        self.rows = rows
+        self.loading = loading
+        self.error = error
+        self.provenance = provenance
+        self.confidence = confidence
+        self.showsHistoryPlaceholder = showsHistoryPlaceholder
+        self.history = history
+        self.historyLoading = historyLoading
+        self.historyError = historyError
+        self.historyCreditStatus = historyCreditStatus
+        self.onLoadHistory = onLoadHistory
+    }
 
     var body: some View {
         CollapsibleSectionView(title: "Ownership", isCollapsed: $isCollapsed) {
@@ -1342,9 +1443,41 @@ struct OwnershipSectionView: View {
                         MessageRowView(text: error, isError: error != "Unavailable")
                             .padding(.top, 4)
                     }
-                    if showsHistoryPlaceholder {
-                        MessageRowView(text: "Ownership history (coming soon)", isError: false)
-                            .padding(.top, 4)
+                    VStack(alignment: .leading, spacing: 8) {
+                        HStack {
+                            Text("History")
+                                .font(appDensity.font(.caption))
+                                .foregroundStyle(.secondary)
+                            Spacer()
+                            if let historyCreditStatus, let onLoadHistory, history.isEmpty, !historyLoading, !showsHistoryPlaceholder {
+                                Button("Load (\(historyCreditStatus.remaining) left)") {
+                                    onLoadHistory()
+                                }
+                                .buttonStyle(.bordered)
+                                .font(appDensity.font(.caption2))
+                            }
+                        }
+                        if historyLoading {
+                            ProgressView("Loading history…")
+                                .appLoadingStyle()
+                        } else if !history.isEmpty {
+                            ForEach(history) { event in
+                                VStack(alignment: .leading, spacing: 3) {
+                                    Text(event.date.formatted(date: .abbreviated, time: .omitted))
+                                        .font(appDensity.font(.caption2))
+                                        .foregroundStyle(.secondary)
+                                    Text(event.summary)
+                                        .font(appDensity.font(.caption))
+                                    Text(event.source)
+                                        .font(appDensity.font(.caption2))
+                                        .foregroundStyle(.secondary)
+                                }
+                            }
+                        } else if let historyError {
+                            MessageRowView(text: historyError, isError: false)
+                        } else if showsHistoryPlaceholder {
+                            MessageRowView(text: "Ownership history available in Data+", isError: false)
+                        }
                     }
                 }
             }
@@ -1362,6 +1495,41 @@ struct SubdomainsSectionView: View {
     let provenance: SectionProvenance?
     let confidence: ConfidenceLevel?
     let showsExtendedPlaceholder: Bool
+    let extendedCount: Int
+    let extendedLoading: Bool
+    let extendedError: String?
+    let extendedCreditStatus: UsageCreditStatus?
+    let onLoadExtended: (() -> Void)?
+
+    init(
+        isCollapsed: Binding<Bool>,
+        rows: [SubdomainRowViewData],
+        groups: [SubdomainGroup],
+        loading: Bool,
+        error: String?,
+        provenance: SectionProvenance?,
+        confidence: ConfidenceLevel?,
+        showsExtendedPlaceholder: Bool,
+        extendedCount: Int = 0,
+        extendedLoading: Bool = false,
+        extendedError: String? = nil,
+        extendedCreditStatus: UsageCreditStatus? = nil,
+        onLoadExtended: (() -> Void)? = nil
+    ) {
+        _isCollapsed = isCollapsed
+        self.rows = rows
+        self.groups = groups
+        self.loading = loading
+        self.error = error
+        self.provenance = provenance
+        self.confidence = confidence
+        self.showsExtendedPlaceholder = showsExtendedPlaceholder
+        self.extendedCount = extendedCount
+        self.extendedLoading = extendedLoading
+        self.extendedError = extendedError
+        self.extendedCreditStatus = extendedCreditStatus
+        self.onLoadExtended = onLoadExtended
+    }
 
     var body: some View {
         CollapsibleSectionView(title: "Subdomains", isCollapsed: $isCollapsed, subtitle: "\(rows.count) found") {
@@ -1373,10 +1541,17 @@ struct SubdomainsSectionView: View {
                 } else if rows.isEmpty {
                     MessageRowView(text: error ?? "No passive subdomains found", isError: false)
                     if showsExtendedPlaceholder {
-                        MessageRowView(text: "Extended subdomain discovery (Data+)", isError: false)
+                        MessageRowView(text: "Extended subdomain discovery available in Data+", isError: false)
                             .padding(.top, 4)
                     }
                 } else {
+                    if let extendedCreditStatus, let onLoadExtended, extendedCount == 0, !extendedLoading, !showsExtendedPlaceholder {
+                        Button("Load extended results (\(extendedCreditStatus.remaining) left)") {
+                            onLoadExtended()
+                        }
+                        .buttonStyle(.bordered)
+                        .font(appDensity.font(.caption2))
+                    }
                     if !groups.isEmpty {
                         Text("Groups")
                             .font(appDensity.font(.caption2))
@@ -1411,8 +1586,18 @@ struct SubdomainsSectionView: View {
                             }
                         }
                     }
-                    if showsExtendedPlaceholder {
-                        MessageRowView(text: "Extended subdomain discovery (Data+)", isError: false)
+                    if extendedLoading {
+                        ProgressView("Loading extended subdomains…")
+                            .appLoadingStyle()
+                            .padding(.top, 4)
+                    } else if extendedCount > 0 {
+                        MessageRowView(text: "\(extendedCount) extended results included", isError: false)
+                            .padding(.top, 4)
+                    } else if let extendedError {
+                        MessageRowView(text: extendedError, isError: false)
+                            .padding(.top, 4)
+                    } else if showsExtendedPlaceholder {
+                        MessageRowView(text: "Extended subdomain discovery available in Data+", isError: false)
                             .padding(.top, 4)
                     }
                 }
@@ -1422,6 +1607,7 @@ struct SubdomainsSectionView: View {
 }
 
 struct DNSSectionView: View {
+    @Environment(\.appDensity) private var appDensity
     @Binding var isCollapsed: Bool
     let dnssecLabel: String?
     let patternSummary: DNSPatternSummary?
@@ -1431,6 +1617,46 @@ struct DNSSectionView: View {
     let dnsProvenance: SectionProvenance?
     let ptrProvenance: SectionProvenance?
     let sectionError: String?
+    let history: [DNSHistoryEvent]
+    let historyLoading: Bool
+    let historyError: String?
+    let showsHistoryPlaceholder: Bool
+    let historyCreditStatus: UsageCreditStatus?
+    let onLoadHistory: (() -> Void)?
+
+    init(
+        isCollapsed: Binding<Bool>,
+        dnssecLabel: String?,
+        patternSummary: DNSPatternSummary?,
+        sections: [DNSRecordSectionViewData],
+        ptrMessage: SectionMessageViewData?,
+        loading: Bool,
+        dnsProvenance: SectionProvenance?,
+        ptrProvenance: SectionProvenance?,
+        sectionError: String?,
+        history: [DNSHistoryEvent] = [],
+        historyLoading: Bool = false,
+        historyError: String? = nil,
+        showsHistoryPlaceholder: Bool = false,
+        historyCreditStatus: UsageCreditStatus? = nil,
+        onLoadHistory: (() -> Void)? = nil
+    ) {
+        _isCollapsed = isCollapsed
+        self.dnssecLabel = dnssecLabel
+        self.patternSummary = patternSummary
+        self.sections = sections
+        self.ptrMessage = ptrMessage
+        self.loading = loading
+        self.dnsProvenance = dnsProvenance
+        self.ptrProvenance = ptrProvenance
+        self.sectionError = sectionError
+        self.history = history
+        self.historyLoading = historyLoading
+        self.historyError = historyError
+        self.showsHistoryPlaceholder = showsHistoryPlaceholder
+        self.historyCreditStatus = historyCreditStatus
+        self.onLoadHistory = onLoadHistory
+    }
 
     var body: some View {
         CollapsibleSectionView(title: "DNS", isCollapsed: $isCollapsed, subtitle: dnssecLabel) {
@@ -1491,6 +1717,50 @@ struct DNSSectionView: View {
                         MessageRowView(text: ptrMessage.text, isError: ptrMessage.isError)
                     }
                 }
+
+                CardView(allowsHorizontalScroll: false) {
+                    HStack {
+                        Text("History")
+                            .font(appDensity.font(.subheadline, weight: .semibold))
+                            .foregroundStyle(.cyan)
+                        Spacer()
+                        if let historyCreditStatus, let onLoadHistory, history.isEmpty, !historyLoading, !showsHistoryPlaceholder {
+                            Button("Load (\(historyCreditStatus.remaining) left)") {
+                                onLoadHistory()
+                            }
+                            .buttonStyle(.bordered)
+                            .font(appDensity.font(.caption2))
+                        }
+                    }
+                    if historyLoading {
+                        ProgressView("Loading DNS history…")
+                            .appLoadingStyle()
+                    } else if !history.isEmpty {
+                        ForEach(history) { event in
+                            VStack(alignment: .leading, spacing: 3) {
+                                Text(event.date.formatted(date: .abbreviated, time: .omitted))
+                                    .font(appDensity.font(.caption2))
+                                    .foregroundStyle(.secondary)
+                                Text(event.summary)
+                                    .font(appDensity.font(.caption))
+                                if !event.aRecords.isEmpty {
+                                    Text("A: \(event.aRecords.joined(separator: ", "))")
+                                        .font(appDensity.font(.caption2))
+                                        .foregroundStyle(.secondary)
+                                }
+                                if !event.nameservers.isEmpty {
+                                    Text("NS: \(event.nameservers.joined(separator: ", "))")
+                                        .font(appDensity.font(.caption2))
+                                        .foregroundStyle(.secondary)
+                                }
+                            }
+                        }
+                    } else if let historyError {
+                        MessageRowView(text: historyError, isError: false)
+                    } else if showsHistoryPlaceholder {
+                        MessageRowView(text: "DNS history available in Data+", isError: false)
+                    }
+                }
             }
         }
     }
@@ -2220,11 +2490,11 @@ struct SettingsView: View {
                 }
             }
 
-            Section("Pro") {
+            Section("Tier") {
                 LabeledContent("Status", value: purchaseService.currentTier.title)
 
                 if purchaseService.currentTier == .free {
-                    Button("Upgrade to Pro") {
+                    Button("Upgrade") {
                         viewModel.isPaywallPresented = true
                     }
                 } else {
@@ -2255,6 +2525,23 @@ struct SettingsView: View {
                 }
             }
 
+            Section("Data+ Usage") {
+                ForEach(UsageCreditFeature.allCases) { feature in
+                    let status = viewModel.usageCredits[feature] ?? UsageCreditStatus(
+                        feature: feature,
+                        remaining: feature.defaultAllowance,
+                        total: feature.defaultAllowance,
+                        resetContext: "Resets with app version \(AppVersion.current)"
+                    )
+                    VStack(alignment: .leading, spacing: 2) {
+                        LabeledContent(feature.title, value: status.summary)
+                        Text(status.resetContext)
+                            .font(appDensity.font(.caption, design: .default))
+                            .foregroundStyle(.secondary)
+                    }
+                }
+            }
+
             Section("Features") {
                 ForEach(FeatureAccessService.enabledFeatureLabels(), id: \.self) { label in
                     Text(label)
@@ -2264,7 +2551,7 @@ struct SettingsView: View {
             Section("About") {
                 LabeledContent("Version", value: appVersion)
                 LabeledContent("Storage", value: "Local-only")
-                LabeledContent("Report Schema", value: "3.1.0")
+                LabeledContent("Report Schema", value: "3.2.0")
             }
         }
         .navigationTitle("Settings")
@@ -2304,6 +2591,9 @@ struct SettingsView: View {
             let currentResolverURL = storedResolverURL.trimmingCharacters(in: .whitespacesAndNewlines)
             resolverOption = DNSResolverOption.option(for: currentResolverURL)
             customResolverURL = resolverOption == .custom ? currentResolverURL : DNSResolverOption.defaultURLString
+            Task {
+                await viewModel.refreshUsageCredits()
+            }
         }
         .onChange(of: resolverOption) { _, newValue in
             guard let presetURL = newValue.urlString else {
diff --git a/DomainDig/DataAccessService.swift b/DomainDig/DataAccessService.swift
index d0254ac..98e215d 100644
--- a/DomainDig/DataAccessService.swift
+++ b/DomainDig/DataAccessService.swift
@@ -10,7 +10,7 @@ enum DataAccessService {
         case .extendedSubdomains:
             return FeatureAccessService.hasAccess(to: .extendedSubdomains)
         case .domainPricing:
-            return false
+            return FeatureAccessService.hasAccess(to: .domainPricing)
         }
     }
 }
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index 6ec7e64..83d282c 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -147,6 +147,9 @@ final class DomainViewModel {
     var ownershipResult: DomainOwnership?
     var ownershipLoading = false
     var ownershipError: String?
+    var ownershipHistory: [DomainOwnershipHistoryEvent] = []
+    var ownershipHistoryLoading = false
+    var ownershipHistoryError: String?
 
     var ptrRecord: String?
     var ptrLoading = false
@@ -159,6 +162,16 @@ final class DomainViewModel {
     var subdomains: [DiscoveredSubdomain] = []
     var subdomainsLoading = false
     var subdomainsError: String?
+    var extendedSubdomains: [DiscoveredSubdomain] = []
+    var extendedSubdomainsLoading = false
+    var extendedSubdomainsError: String?
+    var dnsHistory: [DNSHistoryEvent] = []
+    var dnsHistoryLoading = false
+    var dnsHistoryError: String?
+    var domainPricing: DomainPricingInsight?
+    var domainPricingLoading = false
+    var domainPricingError: String?
+    var usageCredits: [UsageCreditFeature: UsageCreditStatus] = DomainViewModel.defaultUsageCredits()
 
     var portScanResults: [PortScanResult] = []
     var portScanLoading = false
@@ -263,8 +276,12 @@ final class DomainViewModel {
         if sslLoading || hstsLoading { labels.append("TLS") }
         if httpHeadersLoading { labels.append("HTTP") }
         if ownershipLoading { labels.append("Ownership") }
+        if ownershipHistoryLoading { labels.append("Ownership History") }
         if emailSecurityLoading { labels.append("Email") }
         if subdomainsLoading { labels.append("Subdomains") }
+        if extendedSubdomainsLoading { labels.append("Extended Subdomains") }
+        if dnsHistoryLoading { labels.append("DNS History") }
+        if domainPricingLoading { labels.append("Pricing") }
         if redirectChainLoading { labels.append("Redirects") }
         if reachabilityLoading { labels.append("Reachability") }
         if ipGeolocationLoading { labels.append("Geolocation") }
@@ -438,12 +455,20 @@ final class DomainViewModel {
             emailSecurityError: emailSecurityError,
             ownership: ownershipResult,
             ownershipError: ownershipError,
+            ownershipHistory: ownershipHistory,
+            ownershipHistoryError: ownershipHistoryError,
             ptrRecord: ptrRecord,
             ptrError: ptrError,
             redirectChain: redirectChain,
             redirectChainError: redirectChainError,
             subdomains: subdomains,
             subdomainsError: subdomainsError,
+            extendedSubdomains: extendedSubdomains,
+            extendedSubdomainsError: extendedSubdomainsError,
+            dnsHistory: dnsHistory,
+            dnsHistoryError: dnsHistoryError,
+            domainPricing: domainPricing,
+            domainPricingError: domainPricingError,
             portScanResults: allPortScanResults,
             portScanError: combinedPortScanError,
             changeSummary: currentChangeSummary,
@@ -482,6 +507,23 @@ final class DomainViewModel {
         currentReport?.web
     }
 
+    var ownershipHistoryCreditStatus: UsageCreditStatus {
+        usageCredits[.ownershipHistory] ?? Self.fallbackCreditStatus(for: .ownershipHistory)
+    }
+
+    var dnsHistoryCreditStatus: UsageCreditStatus {
+        usageCredits[.dnsHistory] ?? Self.fallbackCreditStatus(for: .dnsHistory)
+    }
+
+    var extendedSubdomainsCreditStatus: UsageCreditStatus {
+        usageCredits[.extendedSubdomains] ?? Self.fallbackCreditStatus(for: .extendedSubdomains)
+    }
+
+    var combinedSubdomains: [DiscoveredSubdomain] {
+        let existingHosts = Set(subdomains.map { $0.hostname.lowercased() })
+        return subdomains + extendedSubdomains.filter { !existingHosts.contains($0.hostname.lowercased()) }
+    }
+
     var summaryFields: [SummaryFieldViewData] {
         Self.summaryFields(from: currentSnapshot)
     }
@@ -527,7 +569,7 @@ final class DomainViewModel {
     }
 
     var subdomainRows: [SubdomainRowViewData] {
-        Self.subdomainRows(from: currentSnapshot)
+        Self.subdomainRows(from: combinedSubdomains)
     }
 
     var reachabilityRows: [ReachabilityRowViewData] {
@@ -934,6 +976,151 @@ final class DomainViewModel {
         return String(data: data, encoding: .utf8)
     }
 
+    func loadOwnershipHistory() async {
+        guard !searchedDomain.isEmpty else { return }
+        guard DataAccessService.hasAccess(to: .ownershipHistory) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .ownershipHistory)
+            return
+        }
+        guard ownershipHistory.isEmpty else { return }
+
+        let creditStatus = await UsageCreditService.shared.status(for: .ownershipHistory)
+        guard !creditStatus.isExhausted else {
+            ownershipHistoryError = "No ownership history credits remaining"
+            await refreshUsageCredits()
+            return
+        }
+
+        ownershipHistoryLoading = true
+        ownershipHistoryError = nil
+
+        let outcome = await ExternalDataService.shared.ownershipHistory(
+            domain: searchedDomain,
+            currentOwnership: ownershipResult,
+            historyEntries: history
+        )
+
+        switch outcome.value {
+        case let .success(events):
+            ownershipHistory = events
+            ownershipHistoryError = nil
+            if outcome.source != .cached {
+                _ = await UsageCreditService.shared.consume(.ownershipHistory)
+            }
+        case let .empty(message):
+            ownershipHistory = []
+            ownershipHistoryError = message
+            if outcome.source != .cached {
+                _ = await UsageCreditService.shared.consume(.ownershipHistory)
+            }
+        case let .error(message):
+            ownershipHistory = []
+            ownershipHistoryError = conciseExternalMessage(message, fallback: "Ownership history unavailable")
+        }
+
+        ownershipHistoryLoading = false
+        _ = saveHistoryEntry(replaceLatest: true)
+        await refreshUsageCredits()
+    }
+
+    func loadDNSHistory() async {
+        guard !searchedDomain.isEmpty else { return }
+        guard DataAccessService.hasAccess(to: .dnsHistory) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .dnsHistory)
+            return
+        }
+        guard dnsHistory.isEmpty else { return }
+
+        let creditStatus = await UsageCreditService.shared.status(for: .dnsHistory)
+        guard !creditStatus.isExhausted else {
+            dnsHistoryError = "No DNS history credits remaining"
+            await refreshUsageCredits()
+            return
+        }
+
+        dnsHistoryLoading = true
+        dnsHistoryError = nil
+
+        let outcome = await ExternalDataService.shared.dnsHistory(
+            domain: searchedDomain,
+            dnsSections: dnsSections,
+            historyEntries: history
+        )
+
+        switch outcome.value {
+        case let .success(events):
+            dnsHistory = events
+            dnsHistoryError = nil
+            if outcome.source != .cached {
+                _ = await UsageCreditService.shared.consume(.dnsHistory)
+            }
+        case let .empty(message):
+            dnsHistory = []
+            dnsHistoryError = message
+            if outcome.source != .cached {
+                _ = await UsageCreditService.shared.consume(.dnsHistory)
+            }
+        case let .error(message):
+            dnsHistory = []
+            dnsHistoryError = conciseExternalMessage(message, fallback: "DNS history unavailable")
+        }
+
+        dnsHistoryLoading = false
+        _ = saveHistoryEntry(replaceLatest: true)
+        await refreshUsageCredits()
+    }
+
+    func loadExtendedSubdomains() async {
+        guard !searchedDomain.isEmpty else { return }
+        guard DataAccessService.hasAccess(to: .extendedSubdomains) else {
+            upgradePrompt = FeatureAccessService.upgradePrompt(for: .extendedSubdomains)
+            return
+        }
+        guard extendedSubdomains.isEmpty else { return }
+
+        let creditStatus = await UsageCreditService.shared.status(for: .extendedSubdomains)
+        guard !creditStatus.isExhausted else {
+            extendedSubdomainsError = "No extended subdomain credits remaining"
+            await refreshUsageCredits()
+            return
+        }
+
+        extendedSubdomainsLoading = true
+        extendedSubdomainsError = nil
+
+        let outcome = await ExternalDataService.shared.extendedSubdomains(
+            domain: searchedDomain,
+            existing: subdomains
+        )
+
+        switch outcome.value {
+        case let .success(results):
+            extendedSubdomains = results
+            extendedSubdomainsError = nil
+            if outcome.source != .cached {
+                _ = await UsageCreditService.shared.consume(.extendedSubdomains)
+            }
+        case let .empty(message):
+            extendedSubdomains = []
+            extendedSubdomainsError = message
+            if outcome.source != .cached {
+                _ = await UsageCreditService.shared.consume(.extendedSubdomains)
+            }
+        case let .error(message):
+            extendedSubdomains = []
+            extendedSubdomainsError = conciseExternalMessage(message, fallback: "Extended subdomains unavailable")
+        }
+
+        extendedSubdomainsLoading = false
+        _ = saveHistoryEntry(replaceLatest: true)
+        await refreshUsageCredits()
+    }
+
+    func refreshUsageCredits() async {
+        let statuses = await UsageCreditService.shared.allStatuses()
+        usageCredits = Dictionary(uniqueKeysWithValues: statuses.map { ($0.feature, $0) })
+    }
+
     func exportBatchText() -> String {
         DomainReportExporter.batchText(
             for: currentBatchReports(),
@@ -1019,11 +1206,18 @@ final class DomainViewModel {
         lastLookupDurationMs = snapshot.totalLookupDurationMs
         refreshingTrackedDomainID = nil
 
+        if DataAccessService.hasAccess(to: .domainPricing), domainPricing == nil {
+            await refreshDomainPricing(for: snapshot.domain, persistAfterFetch: false)
+        }
+
         guard snapshot.statusMessage == nil else {
+            await refreshUsageCredits()
             return history.first(where: { $0.id == snapshot.historyEntryID })
         }
 
-        return saveHistoryEntry(replaceLatest: false)
+        let entry = saveHistoryEntry(replaceLatest: false)
+        await refreshUsageCredits()
+        return entry
     }
 
     private func applySnapshot(_ snapshot: LookupSnapshot) {
@@ -1066,12 +1260,20 @@ final class DomainViewModel {
         emailSecurityError = snapshot.emailSecurityError
         ownershipResult = snapshot.ownership
         ownershipError = snapshot.ownershipError
+        ownershipHistory = snapshot.ownershipHistory
+        ownershipHistoryError = snapshot.ownershipHistoryError
         ptrRecord = snapshot.ptrRecord
         ptrError = snapshot.ptrError
         redirectChain = snapshot.redirectChain
         redirectChainError = snapshot.redirectChainError
         subdomains = snapshot.subdomains
         subdomainsError = snapshot.subdomainsError
+        extendedSubdomains = snapshot.extendedSubdomains
+        extendedSubdomainsError = snapshot.extendedSubdomainsError
+        dnsHistory = snapshot.dnsHistory
+        dnsHistoryError = snapshot.dnsHistoryError
+        domainPricing = snapshot.domainPricing
+        domainPricingError = snapshot.domainPricingError
         portScanResults = snapshot.portScanResults.filter { $0.kind == .standard }
         customPortResults = snapshot.portScanResults.filter { $0.kind == .custom }
         portScanError = snapshot.portScanError
@@ -1087,9 +1289,13 @@ final class DomainViewModel {
         ipGeolocationLoading = false
         emailSecurityLoading = false
         ownershipLoading = false
+        ownershipHistoryLoading = false
         ptrLoading = false
         redirectChainLoading = false
         subdomainsLoading = false
+        extendedSubdomainsLoading = false
+        dnsHistoryLoading = false
+        domainPricingLoading = false
         portScanLoading = false
         customPortScanLoading = false
     }
@@ -1144,12 +1350,20 @@ final class DomainViewModel {
             emailSecurityError: previousSnapshot.emailSecurityError,
             ownership: previousSnapshot.ownership,
             ownershipError: previousSnapshot.ownershipError,
+            ownershipHistory: previousSnapshot.ownershipHistory,
+            ownershipHistoryError: previousSnapshot.ownershipHistoryError,
             ptrRecord: previousSnapshot.ptrRecord,
             ptrError: previousSnapshot.ptrError,
             redirectChain: previousSnapshot.redirectChain,
             redirectChainError: previousSnapshot.redirectChainError,
             subdomains: previousSnapshot.subdomains,
             subdomainsError: previousSnapshot.subdomainsError,
+            extendedSubdomains: previousSnapshot.extendedSubdomains,
+            extendedSubdomainsError: previousSnapshot.extendedSubdomainsError,
+            dnsHistory: previousSnapshot.dnsHistory,
+            dnsHistoryError: previousSnapshot.dnsHistoryError,
+            domainPricing: previousSnapshot.domainPricing,
+            domainPricingError: previousSnapshot.domainPricingError,
             portScanResults: previousSnapshot.portScanResults,
             portScanError: previousSnapshot.portScanError,
             changeSummary: previousSnapshot.changeSummary,
@@ -1515,9 +1729,13 @@ final class DomainViewModel {
             emailSecurity: snapshot.emailSecurity,
             mtaSts: snapshot.emailSecurity?.mtaSts,
             ownership: snapshot.ownership,
+            ownershipHistory: snapshot.ownershipHistory,
             ptrRecord: snapshot.ptrRecord,
             redirectChain: snapshot.redirectChain,
             subdomains: snapshot.subdomains,
+            extendedSubdomains: snapshot.extendedSubdomains,
+            dnsHistory: snapshot.dnsHistory,
+            domainPricing: snapshot.domainPricing,
             portScanResults: snapshot.portScanResults,
             hstsPreloaded: snapshot.hstsPreloaded,
             availabilityResult: snapshot.availabilityResult,
@@ -1549,9 +1767,13 @@ final class DomainViewModel {
             ipGeolocationError: snapshot.ipGeolocationError,
             emailSecurityError: snapshot.emailSecurityError,
             ownershipError: snapshot.ownershipError,
+            ownershipHistoryError: snapshot.ownershipHistoryError,
             ptrError: snapshot.ptrError,
             redirectChainError: snapshot.redirectChainError,
             subdomainsError: snapshot.subdomainsError,
+            extendedSubdomainsError: snapshot.extendedSubdomainsError,
+            dnsHistoryError: snapshot.dnsHistoryError,
+            domainPricingError: snapshot.domainPricingError,
             portScanError: snapshot.portScanError
         )
 
@@ -2038,6 +2260,9 @@ final class DomainViewModel {
         ownershipResult = nil
         ownershipError = nil
         ownershipLoading = false
+        ownershipHistory = []
+        ownershipHistoryError = nil
+        ownershipHistoryLoading = false
         ptrRecord = nil
         ptrError = nil
         ptrLoading = false
@@ -2047,6 +2272,15 @@ final class DomainViewModel {
         subdomains = []
         subdomainsError = nil
         subdomainsLoading = false
+        extendedSubdomains = []
+        extendedSubdomainsError = nil
+        extendedSubdomainsLoading = false
+        dnsHistory = []
+        dnsHistoryError = nil
+        dnsHistoryLoading = false
+        domainPricing = nil
+        domainPricingError = nil
+        domainPricingLoading = false
         portScanResults = []
         portScanError = nil
         portScanLoading = false
@@ -2072,9 +2306,13 @@ final class DomainViewModel {
         ipGeolocationLoading = loading
         emailSecurityLoading = loading
         ownershipLoading = loading
+        ownershipHistoryLoading = false
         ptrLoading = loading
         redirectChainLoading = loading
         subdomainsLoading = loading
+        extendedSubdomainsLoading = false
+        dnsHistoryLoading = false
+        domainPricingLoading = false
         portScanLoading = loading
     }
 
@@ -2248,12 +2486,20 @@ final class DomainViewModel {
             emailSecurityError: nil,
             ownership: nil,
             ownershipError: nil,
+            ownershipHistory: [],
+            ownershipHistoryError: nil,
             ptrRecord: nil,
             ptrError: nil,
             redirectChain: [],
             redirectChainError: nil,
             subdomains: [],
             subdomainsError: nil,
+            extendedSubdomains: [],
+            extendedSubdomainsError: nil,
+            dnsHistory: [],
+            dnsHistoryError: nil,
+            domainPricing: nil,
+            domainPricingError: nil,
             portScanResults: [],
             portScanError: nil,
             changeSummary: trackedDomain.lastChangeSummary,
@@ -2435,6 +2681,30 @@ final class DomainViewModel {
                 at: 3
             )
         }
+        if let pricing = snapshot.domainPricing {
+            rows.append(
+                InfoRowViewData(
+                    label: "External Price",
+                    value: pricing.estimatedPrice ?? "Unavailable",
+                    tone: .secondary
+                )
+            )
+            if let premiumIndicator = pricing.premiumIndicator {
+                rows.append(
+                    InfoRowViewData(
+                        label: "Premium",
+                        value: premiumIndicator ? "Yes" : "No",
+                        tone: premiumIndicator ? .warning : .secondary
+                    )
+                )
+            }
+            if let resaleSignal = pricing.resaleSignal {
+                rows.append(InfoRowViewData(label: "Resale", value: resaleSignal, tone: .secondary))
+            }
+            if let auctionSignal = pricing.auctionSignal {
+                rows.append(InfoRowViewData(label: "Auction", value: auctionSignal, tone: .secondary))
+            }
+        }
         if let certificateStatus = certificateBadgeLabel(from: snapshot) {
             rows.insert(
                 InfoRowViewData(
@@ -2460,6 +2730,15 @@ final class DomainViewModel {
         }
     }
 
+    static func subdomainRows(from subdomains: [DiscoveredSubdomain]) -> [SubdomainRowViewData] {
+        subdomains.map { subdomain in
+            SubdomainRowViewData(
+                hostname: subdomain.hostname,
+                isInteresting: subdomain.isExtended || isInterestingSubdomain(subdomain.hostname)
+            )
+        }
+    }
+
     static func dnsRows(from snapshot: LookupSnapshot) -> [DNSRecordSectionViewData] {
         snapshot.dnsSections.map { section in
             DNSRecordSectionViewData(
@@ -3098,6 +3377,61 @@ final class DomainViewModel {
         return formatter
     }()
 
+    private func refreshDomainPricing(for domain: String, persistAfterFetch: Bool) async {
+        domainPricingLoading = true
+        let outcome = await ExternalDataService.shared.pricing(domain: domain)
+
+        switch outcome.value {
+        case let .success(pricing):
+            domainPricing = pricing
+            domainPricingError = nil
+        case let .empty(message):
+            domainPricing = nil
+            domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable")
+        case let .error(message):
+            domainPricing = nil
+            domainPricingError = conciseExternalMessage(message, fallback: "External pricing unavailable")
+        }
+
+        domainPricingLoading = false
+
+        if persistAfterFetch {
+            _ = saveHistoryEntry(replaceLatest: true)
+        }
+    }
+
+    private func conciseExternalMessage(_ message: String, fallback: String) -> String {
+        let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
+        if trimmed.isEmpty {
+            return fallback
+        }
+        if trimmed.localizedCaseInsensitiveContains("rate") {
+            return "Rate limited. Try again later."
+        }
+        if trimmed.localizedCaseInsensitiveContains("invalid") {
+            return "External data was invalid."
+        }
+        if trimmed.localizedCaseInsensitiveContains("network") {
+            return "External data is offline."
+        }
+        return trimmed
+    }
+
+    private static func defaultUsageCredits() -> [UsageCreditFeature: UsageCreditStatus] {
+        Dictionary(uniqueKeysWithValues: UsageCreditFeature.allCases.map { feature in
+            (feature, fallbackCreditStatus(for: feature))
+        })
+    }
+
+    private static func fallbackCreditStatus(for feature: UsageCreditFeature) -> UsageCreditStatus {
+        UsageCreditStatus(
+            feature: feature,
+            remaining: feature.defaultAllowance,
+            total: feature.defaultAllowance,
+            resetContext: "Resets with app version \(AppVersion.current)"
+        )
+    }
+
     private static func csvEscaped(_ value: String) -> String {
         let escaped = value.replacingOccurrences(of: "\"", with: "\"\"")
         return "\"\(escaped)\""
diff --git a/DomainDig/ExternalDataService.swift b/DomainDig/ExternalDataService.swift
new file mode 100644
index 0000000..5b4bf66
--- /dev/null
+++ b/DomainDig/ExternalDataService.swift
@@ -0,0 +1,627 @@
+import Foundation
+
+actor ExternalDataService {
+    static let shared = ExternalDataService()
+
+    private enum RequestKey: Hashable {
+        case ownershipHistory(String)
+        case dnsHistory(String)
+        case extendedSubdomains(String)
+        case pricing(String)
+    }
+
+    private enum RateLimitBucket: Hashable {
+        case history
+        case subdomains
+        case pricing
+
+        var minimumSpacing: TimeInterval {
+            switch self {
+            case .history:
+                return 1.0
+            case .subdomains:
+                return 1.5
+            case .pricing:
+                return 1.0
+            }
+        }
+    }
+
+    private enum CachedPayload {
+        case ownershipHistory(ServiceResult<[DomainOwnershipHistoryEvent]>)
+        case dnsHistory(ServiceResult<[DNSHistoryEvent]>)
+        case extendedSubdomains(ServiceResult<[DiscoveredSubdomain]>)
+        case pricing(ServiceResult<DomainPricingInsight>)
+    }
+
+    private struct CacheEntry {
+        let payload: CachedPayload
+        let expiresAt: Date
+    }
+
+    private struct Configuration {
+        let ownershipHistoryURL: String?
+        let dnsHistoryURL: String?
+        let extendedSubdomainsURL: String?
+        let pricingURL: String?
+    }
+
+    private let ttl: TimeInterval = 900
+    private let session: URLSession = .shared
+    private var cache: [RequestKey: CacheEntry] = [:]
+    private var inFlight: [RequestKey: Task<CachedPayload, Never>] = [:]
+    private var nextAllowedAt: [RateLimitBucket: Date] = [:]
+
+    func clearCache() {
+        cache.removeAll()
+        inFlight.values.forEach { $0.cancel() }
+        inFlight.removeAll()
+        nextAllowedAt.removeAll()
+    }
+
+    func ownershipHistory(
+        domain: String,
+        currentOwnership: DomainOwnership?,
+        historyEntries: [HistoryEntry]
+    ) async -> CachedLookupResult<ServiceResult<[DomainOwnershipHistoryEvent]>> {
+        let normalizedDomain = Self.normalize(domain)
+        return await execute(
+            key: .ownershipHistory(normalizedDomain),
+            rateLimitBucket: .history,
+            extract: { payload in
+                guard case let .ownershipHistory(result) = payload else { return nil }
+                return result
+            },
+            operation: { [configuration = configuration()] in
+                let localEvents = Self.localOwnershipHistory(
+                    domain: normalizedDomain,
+                    currentOwnership: currentOwnership,
+                    historyEntries: historyEntries
+                )
+                let externalEvents = await self.fetchOwnershipHistory(domain: normalizedDomain, configuration: configuration)
+                return .ownershipHistory(Self.mergeOwnershipHistory(localEvents: localEvents, externalEvents: externalEvents))
+            }
+        )
+    }
+
+    func dnsHistory(
+        domain: String,
+        dnsSections: [DNSSection],
+        historyEntries: [HistoryEntry]
+    ) async -> CachedLookupResult<ServiceResult<[DNSHistoryEvent]>> {
+        let normalizedDomain = Self.normalize(domain)
+        return await execute(
+            key: .dnsHistory(normalizedDomain),
+            rateLimitBucket: .history,
+            extract: { payload in
+                guard case let .dnsHistory(result) = payload else { return nil }
+                return result
+            },
+            operation: { [configuration = configuration()] in
+                let localEvents = Self.localDNSHistory(
+                    domain: normalizedDomain,
+                    dnsSections: dnsSections,
+                    historyEntries: historyEntries
+                )
+                let externalEvents = await self.fetchDNSHistory(domain: normalizedDomain, configuration: configuration)
+                return .dnsHistory(Self.mergeDNSHistory(localEvents: localEvents, externalEvents: externalEvents))
+            }
+        )
+    }
+
+    func extendedSubdomains(
+        domain: String,
+        existing: [DiscoveredSubdomain]
+    ) async -> CachedLookupResult<ServiceResult<[DiscoveredSubdomain]>> {
+        let normalizedDomain = Self.normalize(domain)
+        return await execute(
+            key: .extendedSubdomains(normalizedDomain),
+            rateLimitBucket: .subdomains,
+            extract: { payload in
+                guard case let .extendedSubdomains(result) = payload else { return nil }
+                return result
+            },
+            operation: { [configuration = configuration()] in
+                var merged = existing
+                switch await SubdomainDiscoveryService.discover(for: normalizedDomain, limit: 100) {
+                case let .success(results):
+                    merged = Self.mergeSubdomains(primary: merged, additional: results.map {
+                        DiscoveredSubdomain(hostname: $0.hostname, source: $0.source ?? "crt.sh", isExtended: true)
+                    })
+                case .empty, .error:
+                    break
+                }
+
+                if let external = await self.fetchExtendedSubdomains(domain: normalizedDomain, configuration: configuration) {
+                    merged = Self.mergeSubdomains(primary: merged, additional: external)
+                }
+
+                if merged.count <= existing.count {
+                    return .extendedSubdomains(.empty("No extended subdomains available"))
+                }
+
+                let onlyExtended = merged.filter(\.isExtended)
+                return .extendedSubdomains(.success(onlyExtended.sorted { $0.hostname < $1.hostname }))
+            }
+        )
+    }
+
+    func pricing(domain: String) async -> CachedLookupResult<ServiceResult<DomainPricingInsight>> {
+        let normalizedDomain = Self.normalize(domain)
+        return await execute(
+            key: .pricing(normalizedDomain),
+            rateLimitBucket: .pricing,
+            extract: { payload in
+                guard case let .pricing(result) = payload else { return nil }
+                return result
+            },
+            operation: { [configuration = configuration()] in
+                .pricing(await self.fetchPricing(domain: normalizedDomain, configuration: configuration))
+            }
+        )
+    }
+
+    private func execute<T>(
+        key: RequestKey,
+        rateLimitBucket: RateLimitBucket,
+        extract: @escaping (CachedPayload) -> T?,
+        operation: @escaping @Sendable () async -> CachedPayload
+    ) async -> CachedLookupResult<T> {
+        if let cachedEntry = cache[key], cachedEntry.expiresAt > Date(), let value = extract(cachedEntry.payload) {
+            return CachedLookupResult(value: value, source: .cached)
+        }
+
+        if let task = inFlight[key], let value = extract(await task.value) {
+            return CachedLookupResult(value: value, source: .mixed)
+        }
+
+        let task = Task<CachedPayload, Never> {
+            await self.enforceRateLimit(for: rateLimitBucket)
+            return await operation()
+        }
+        inFlight[key] = task
+
+        let payload = await task.value
+        cache[key] = CacheEntry(payload: payload, expiresAt: Date().addingTimeInterval(ttl))
+        inFlight[key] = nil
+
+        guard let value = extract(payload) else {
+            fatalError("ExternalDataService payload extraction mismatch")
+        }
+
+        return CachedLookupResult(value: value, source: .live)
+    }
+
+    private func enforceRateLimit(for bucket: RateLimitBucket) async {
+        let now = Date()
+        if let nextAllowed = nextAllowedAt[bucket], nextAllowed > now {
+            let delay = nextAllowed.timeIntervalSince(now)
+            if delay > 0 {
+                try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
+            }
+        }
+        nextAllowedAt[bucket] = Date().addingTimeInterval(bucket.minimumSpacing)
+    }
+
+    private func configuration() -> Configuration {
+        let defaults = UserDefaults.standard
+        return Configuration(
+            ownershipHistoryURL: defaults.string(forKey: "externalData.ownershipHistoryURL")
+                ?? Bundle.main.object(forInfoDictionaryKey: "ExternalOwnershipHistoryURL") as? String,
+            dnsHistoryURL: defaults.string(forKey: "externalData.dnsHistoryURL")
+                ?? Bundle.main.object(forInfoDictionaryKey: "ExternalDNSHistoryURL") as? String,
+            extendedSubdomainsURL: defaults.string(forKey: "externalData.extendedSubdomainsURL")
+                ?? Bundle.main.object(forInfoDictionaryKey: "ExternalExtendedSubdomainsURL") as? String,
+            pricingURL: defaults.string(forKey: "externalData.pricingURL")
+                ?? Bundle.main.object(forInfoDictionaryKey: "ExternalPricingURL") as? String
+        )
+    }
+
+    private func fetchOwnershipHistory(
+        domain: String,
+        configuration: Configuration
+    ) async -> [DomainOwnershipHistoryEvent] {
+        guard let template = configuration.ownershipHistoryURL,
+              let url = Self.url(from: template, domain: domain) else {
+            return []
+        }
+
+        switch await requestData(url: url) {
+        case let .success(data):
+            return parseOwnershipHistoryEvents(from: data)
+        case .empty, .error:
+            return []
+        }
+    }
+
+    private func fetchDNSHistory(
+        domain: String,
+        configuration: Configuration
+    ) async -> [DNSHistoryEvent] {
+        guard let template = configuration.dnsHistoryURL,
+              let url = Self.url(from: template, domain: domain) else {
+            return []
+        }
+
+        switch await requestData(url: url) {
+        case let .success(data):
+            return parseDNSHistoryEvents(from: data)
+        case .empty, .error:
+            return []
+        }
+    }
+
+    private func fetchExtendedSubdomains(
+        domain: String,
+        configuration: Configuration
+    ) async -> [DiscoveredSubdomain]? {
+        guard let template = configuration.extendedSubdomainsURL,
+              let url = Self.url(from: template, domain: domain) else {
+            return nil
+        }
+
+        switch await requestData(url: url) {
+        case let .success(data):
+            return parseSubdomains(from: data)
+        case .empty, .error:
+            return nil
+        }
+    }
+
+    private func fetchPricing(
+        domain: String,
+        configuration: Configuration
+    ) async -> ServiceResult<DomainPricingInsight> {
+        guard let template = configuration.pricingURL,
+              let url = Self.url(from: template, domain: domain) else {
+            return .empty("External pricing unavailable")
+        }
+
+        switch await requestData(url: url) {
+        case let .success(data):
+            guard let pricing = parsePricing(from: data) else {
+                return .error("Invalid external response")
+            }
+            return .success(pricing)
+        case let .empty(message):
+            return .empty(message)
+        case let .error(message):
+            return .error(message)
+        }
+    }
+
+    private func requestData(url: URL) async -> ServiceResult<Data> {
+        for attempt in 0..<2 {
+            do {
+                let (data, response) = try await session.data(for: URLRequest(url: url, timeoutInterval: 8))
+                guard let httpResponse = response as? HTTPURLResponse else {
+                    return .error("External data unavailable")
+                }
+
+                switch httpResponse.statusCode {
+                case 200:
+                    return .success(data)
+                case 204, 404:
+                    return .empty("No external data available")
+                case 429:
+                    return .error("Rate limit reached")
+                case 500...599 where attempt == 0:
+                    continue
+                default:
+                    return .error("External provider failed")
+                }
+            } catch is DecodingError {
+                return .error("Invalid external response")
+            } catch {
+                if attempt == 0 {
+                    continue
+                }
+                return .error(error.localizedDescription)
+            }
+        }
+
+        return .error("External provider failed")
+    }
+
+    private func parseOwnershipHistoryEvents(from data: Data) -> [DomainOwnershipHistoryEvent] {
+        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+              let rawEvents = json["events"] as? [[String: Any]] else {
+            return []
+        }
+
+        return rawEvents.compactMap { event in
+            guard let dateString = event["date"] as? String,
+                  let date = Self.iso8601DateFormatter.date(from: dateString) else {
+                return nil
+            }
+
+            return DomainOwnershipHistoryEvent(
+                date: date,
+                summary: event["summary"] as? String ?? "Ownership change observed",
+                registrar: event["registrar"] as? String,
+                registrant: event["registrant"] as? String,
+                nameservers: event["nameservers"] as? [String] ?? [],
+                source: event["source"] as? String ?? "Configured external history feed",
+                isExternal: true
+            )
+        }
+    }
+
+    private func parseDNSHistoryEvents(from data: Data) -> [DNSHistoryEvent] {
+        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+              let rawEvents = json["events"] as? [[String: Any]] else {
+            return []
+        }
+
+        return rawEvents.compactMap { event in
+            guard let dateString = event["date"] as? String,
+                  let date = Self.iso8601DateFormatter.date(from: dateString) else {
+                return nil
+            }
+
+            return DNSHistoryEvent(
+                date: date,
+                summary: event["summary"] as? String ?? "DNS change observed",
+                aRecords: event["a_records"] as? [String] ?? [],
+                nameservers: event["nameservers"] as? [String] ?? [],
+                source: event["source"] as? String ?? "Configured external history feed",
+                isExternal: true
+            )
+        }
+    }
+
+    private func parseSubdomains(from data: Data) -> [DiscoveredSubdomain] {
+        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+              let rawSubdomains = json["subdomains"] as? [[String: Any]] else {
+            return []
+        }
+
+        return rawSubdomains.compactMap { item in
+            guard let hostname = item["hostname"] as? String else { return nil }
+            return DiscoveredSubdomain(
+                hostname: hostname,
+                source: item["source"] as? String ?? "Configured external subdomain feed",
+                isExtended: true
+            )
+        }
+    }
+
+    private func parsePricing(from data: Data) -> DomainPricingInsight? {
+        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+            return nil
+        }
+
+        return DomainPricingInsight(
+            estimatedPrice: json["estimated_price"] as? String,
+            premiumIndicator: json["premium"] as? Bool,
+            resaleSignal: json["resale_signal"] as? String,
+            auctionSignal: json["auction_signal"] as? String,
+            source: json["source"] as? String ?? "Configured external pricing feed",
+            collectedAt: Date()
+        )
+    }
+
+    private static func localOwnershipHistory(
+        domain: String,
+        currentOwnership: DomainOwnership?,
+        historyEntries: [HistoryEntry]
+    ) -> [DomainOwnershipHistoryEvent] {
+        let domainHistory = historyEntries
+            .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
+            .sorted { $0.timestamp < $1.timestamp }
+
+        var events: [DomainOwnershipHistoryEvent] = []
+        var previousOwnership: DomainOwnership?
+
+        for entry in domainHistory {
+            guard let ownership = entry.ownership else { continue }
+            let summary = ownershipSummaryChange(previous: previousOwnership, current: ownership)
+            if let summary {
+                events.append(
+                    DomainOwnershipHistoryEvent(
+                        date: entry.timestamp,
+                        summary: summary,
+                        registrar: ownership.registrar,
+                        registrant: ownership.registrant,
+                        nameservers: ownership.nameservers,
+                        source: "Local observations",
+                        isExternal: false
+                    )
+                )
+            }
+            previousOwnership = ownership
+        }
+
+        if let currentOwnership, events.isEmpty {
+            events.append(
+                DomainOwnershipHistoryEvent(
+                    date: Date(),
+                    summary: "Current ownership snapshot",
+                    registrar: currentOwnership.registrar,
+                    registrant: currentOwnership.registrant,
+                    nameservers: currentOwnership.nameservers,
+                    source: "Local observations",
+                    isExternal: false
+                )
+            )
+        }
+
+        return events.sorted { $0.date > $1.date }
+    }
+
+    private static func localDNSHistory(
+        domain: String,
+        dnsSections: [DNSSection],
+        historyEntries: [HistoryEntry]
+    ) -> [DNSHistoryEvent] {
+        let domainHistory = historyEntries
+            .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
+            .sorted { $0.timestamp < $1.timestamp }
+
+        var events: [DNSHistoryEvent] = []
+        var previousARecords: [String] = []
+        var previousNameservers: [String] = []
+
+        for entry in domainHistory {
+            let aRecords = Self.dnsValues(for: .A, in: entry.dnsSections)
+            let nameservers = Self.dnsValues(for: .NS, in: entry.dnsSections)
+            let summary = dnsSummaryChange(
+                previousARecords: previousARecords,
+                currentARecords: aRecords,
+                previousNameservers: previousNameservers,
+                currentNameservers: nameservers
+            )
+
+            if let summary {
+                events.append(
+                    DNSHistoryEvent(
+                        date: entry.timestamp,
+                        summary: summary,
+                        aRecords: aRecords,
+                        nameservers: nameservers,
+                        source: "Local observations",
+                        isExternal: false
+                    )
+                )
+            }
+
+            previousARecords = aRecords
+            previousNameservers = nameservers
+        }
+
+        if events.isEmpty {
+            let currentARecords = Self.dnsValues(for: .A, in: dnsSections)
+            let currentNameservers = Self.dnsValues(for: .NS, in: dnsSections)
+            if !currentARecords.isEmpty || !currentNameservers.isEmpty {
+                events.append(
+                    DNSHistoryEvent(
+                        date: Date(),
+                        summary: "Current DNS snapshot",
+                        aRecords: currentARecords,
+                        nameservers: currentNameservers,
+                        source: "Local observations",
+                        isExternal: false
+                    )
+                )
+            }
+        }
+
+        return events.sorted { $0.date > $1.date }
+    }
+
+    private static func mergeOwnershipHistory(
+        localEvents: [DomainOwnershipHistoryEvent],
+        externalEvents: [DomainOwnershipHistoryEvent]
+    ) -> ServiceResult<[DomainOwnershipHistoryEvent]> {
+        let merged = (externalEvents + localEvents)
+            .sorted { $0.date > $1.date }
+            .reduce(into: [DomainOwnershipHistoryEvent]()) { partialResult, event in
+                let duplicate = partialResult.contains {
+                    $0.date == event.date
+                        && $0.summary == event.summary
+                        && $0.registrar == event.registrar
+                        && $0.nameservers == event.nameservers
+                }
+                if !duplicate {
+                    partialResult.append(event)
+                }
+            }
+
+        return merged.isEmpty ? .empty("No ownership history available") : .success(merged)
+    }
+
+    private static func mergeDNSHistory(
+        localEvents: [DNSHistoryEvent],
+        externalEvents: [DNSHistoryEvent]
+    ) -> ServiceResult<[DNSHistoryEvent]> {
+        let merged = (externalEvents + localEvents)
+            .sorted { $0.date > $1.date }
+            .reduce(into: [DNSHistoryEvent]()) { partialResult, event in
+                let duplicate = partialResult.contains {
+                    $0.date == event.date
+                        && $0.summary == event.summary
+                        && $0.aRecords == event.aRecords
+                        && $0.nameservers == event.nameservers
+                }
+                if !duplicate {
+                    partialResult.append(event)
+                }
+            }
+
+        return merged.isEmpty ? .empty("No DNS history available") : .success(merged)
+    }
+
+    private static func mergeSubdomains(
+        primary: [DiscoveredSubdomain],
+        additional: [DiscoveredSubdomain]
+    ) -> [DiscoveredSubdomain] {
+        var seen = Set(primary.map { $0.hostname.lowercased() })
+        var merged = primary
+
+        for subdomain in additional where seen.insert(subdomain.hostname.lowercased()).inserted {
+            merged.append(subdomain)
+        }
+
+        return merged.sorted { $0.hostname < $1.hostname }
+    }
+
+    private static func ownershipSummaryChange(previous: DomainOwnership?, current: DomainOwnership) -> String? {
+        guard let previous else {
+            return "Initial ownership observation"
+        }
+
+        var changes: [String] = []
+        if previous.registrar != current.registrar {
+            changes.append("Registrar changed")
+        }
+        if previous.registrant != current.registrant, current.registrant != nil {
+            changes.append("Ownership changed")
+        }
+        if previous.nameservers != current.nameservers {
+            changes.append("Nameservers changed")
+        }
+
+        return changes.isEmpty ? nil : changes.joined(separator: " • ")
+    }
+
+    private static func dnsSummaryChange(
+        previousARecords: [String],
+        currentARecords: [String],
+        previousNameservers: [String],
+        currentNameservers: [String]
+    ) -> String? {
+        var changes: [String] = []
+        if previousARecords != currentARecords, !currentARecords.isEmpty {
+            changes.append("A records changed")
+        }
+        if previousNameservers != currentNameservers, !currentNameservers.isEmpty {
+            changes.append("NS records changed")
+        }
+        if previousARecords.isEmpty && previousNameservers.isEmpty && (!currentARecords.isEmpty || !currentNameservers.isEmpty) {
+            changes.append("Initial DNS observation")
+        }
+        return changes.isEmpty ? nil : changes.joined(separator: " • ")
+    }
+
+    private static func url(from template: String, domain: String) -> URL? {
+        URL(string: template.replacingOccurrences(of: "{domain}", with: domain))
+    }
+
+    private static func normalize(_ domain: String) -> String {
+        domain.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+    }
+
+    private static func dnsValues(for type: DNSRecordType, in sections: [DNSSection]) -> [String] {
+        sections
+            .first(where: { $0.recordType == type })?
+            .records
+            .map(\.value)
+            .sorted() ?? []
+    }
+
+    private static let iso8601DateFormatter: ISO8601DateFormatter = {
+        let formatter = ISO8601DateFormatter()
+        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+        return formatter
+    }()
+}
diff --git a/DomainDig/FeatureAccessService.swift b/DomainDig/FeatureAccessService.swift
index d23f348..535ba7c 100644
--- a/DomainDig/FeatureAccessService.swift
+++ b/DomainDig/FeatureAccessService.swift
@@ -29,6 +29,7 @@ enum FeatureCapability: String, CaseIterable, Identifiable {
     case ownershipHistory
     case dnsHistory
     case extendedSubdomains
+    case domainPricing
 
     var id: String { rawValue }
 
@@ -52,6 +53,8 @@ enum FeatureCapability: String, CaseIterable, Identifiable {
             return "DNS history"
         case .extendedSubdomains:
             return "Extended subdomains"
+        case .domainPricing:
+            return "Domain pricing"
         }
     }
 }
@@ -139,7 +142,7 @@ enum FeatureAccessService {
         switch capability {
         case .workflows, .batchOperations, .advancedExports:
             return "Available in Pro"
-        case .ownershipHistory, .dnsHistory, .extendedSubdomains:
+        case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing:
             return "Available in Data+"
         case .limitedTracking:
             return "Tracking is limited on Free"
@@ -199,8 +202,15 @@ enum FeatureAccessService {
     }
 
     static func upgradePrompt(for capability: FeatureCapability) -> UpgradePromptContext {
-        UpgradePromptContext(
-            title: "Available in Pro",
+        let title: String
+        switch capability {
+        case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing:
+            title = "Available in Data+"
+        default:
+            title = "Available in Pro"
+        }
+        return UpgradePromptContext(
+            title: title,
             message: upgradeMessage(for: capability),
             capability: capability
         )
diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift
index e3a77f4..6eb6777 100644
--- a/DomainDig/HistoryView.swift
+++ b/DomainDig/HistoryView.swift
@@ -198,6 +198,9 @@ struct HistoryDetailView: View {
                     trackedDomain: trackedDomain,
                     workflows: viewModel.workflowsContaining(domain: entry.domain),
                     trackingLimitMessage: nil,
+                    pricingLoading: false,
+                    pricingError: snapshot.domainPricingError,
+                    showsPricingPlaceholder: !DataAccessService.hasAccess(to: .domainPricing),
                     onTrack: {
                         _ = viewModel.trackDomain(domain: entry.domain, availabilityStatus: entry.availabilityResult?.status)
                     },
diff --git a/DomainDig/Models.swift b/DomainDig/Models.swift
index 6b7c2c1..530a89e 100644
--- a/DomainDig/Models.swift
+++ b/DomainDig/Models.swift
@@ -370,6 +370,117 @@ enum DataCapability: String, Codable {
     case domainPricing
 }
 
+enum UsageCreditFeature: String, Codable, CaseIterable, Identifiable, Sendable {
+    case ownershipHistory
+    case dnsHistory
+    case extendedSubdomains
+
+    nonisolated var id: String { rawValue }
+
+    nonisolated var title: String {
+        switch self {
+        case .ownershipHistory:
+            return "Ownership history"
+        case .dnsHistory:
+            return "DNS history"
+        case .extendedSubdomains:
+            return "Extended subdomains"
+        }
+    }
+
+    nonisolated var defaultAllowance: Int {
+        switch self {
+        case .ownershipHistory, .dnsHistory:
+            return 8
+        case .extendedSubdomains:
+            return 12
+        }
+    }
+}
+
+struct UsageCreditStatus: Codable, Equatable, Sendable {
+    let feature: UsageCreditFeature
+    let remaining: Int
+    let total: Int
+    let resetContext: String
+
+    nonisolated var summary: String {
+        "\(remaining) uses remaining"
+    }
+
+    nonisolated var isExhausted: Bool {
+        remaining <= 0
+    }
+}
+
+struct DomainOwnershipHistoryEvent: Identifiable, Codable, Equatable, Sendable {
+    let id: UUID
+    let date: Date
+    let summary: String
+    let registrar: String?
+    let registrant: String?
+    let nameservers: [String]
+    let source: String
+    let isExternal: Bool
+
+    nonisolated init(
+        id: UUID = UUID(),
+        date: Date,
+        summary: String,
+        registrar: String? = nil,
+        registrant: String? = nil,
+        nameservers: [String] = [],
+        source: String,
+        isExternal: Bool
+    ) {
+        self.id = id
+        self.date = date
+        self.summary = summary
+        self.registrar = registrar
+        self.registrant = registrant
+        self.nameservers = nameservers
+        self.source = source
+        self.isExternal = isExternal
+    }
+}
+
+struct DNSHistoryEvent: Identifiable, Codable, Equatable, Sendable {
+    let id: UUID
+    let date: Date
+    let summary: String
+    let aRecords: [String]
+    let nameservers: [String]
+    let source: String
+    let isExternal: Bool
+
+    nonisolated init(
+        id: UUID = UUID(),
+        date: Date,
+        summary: String,
+        aRecords: [String] = [],
+        nameservers: [String] = [],
+        source: String,
+        isExternal: Bool
+    ) {
+        self.id = id
+        self.date = date
+        self.summary = summary
+        self.aRecords = aRecords
+        self.nameservers = nameservers
+        self.source = source
+        self.isExternal = isExternal
+    }
+}
+
+struct DomainPricingInsight: Codable, Equatable, Sendable {
+    let estimatedPrice: String?
+    let premiumIndicator: Bool?
+    let resaleSignal: String?
+    let auctionSignal: String?
+    let source: String
+    let collectedAt: Date
+}
+
 enum HistoryDateFilter: String, CaseIterable, Identifiable {
     case today
     case last7Days
@@ -691,6 +802,7 @@ struct IPGeolocation: Codable {
 
 struct DomainOwnership: Codable, Equatable {
     let registrar: String?
+    let registrant: String?
     let createdDate: Date?
     let expirationDate: Date?
     let status: [String]
@@ -699,6 +811,7 @@ struct DomainOwnership: Codable, Equatable {
 
     init(
         registrar: String? = nil,
+        registrant: String? = nil,
         createdDate: Date? = nil,
         expirationDate: Date? = nil,
         status: [String] = [],
@@ -706,6 +819,7 @@ struct DomainOwnership: Codable, Equatable {
         abuseEmail: String? = nil
     ) {
         self.registrar = registrar
+        self.registrant = registrant
         self.createdDate = createdDate
         self.expirationDate = expirationDate
         self.status = status
@@ -716,6 +830,7 @@ struct DomainOwnership: Codable, Equatable {
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         registrar = try container.decodeIfPresent(String.self, forKey: .registrar)
+        registrant = try container.decodeIfPresent(String.self, forKey: .registrant)
         createdDate = try container.decodeIfPresent(Date.self, forKey: .createdDate)
         expirationDate = try container.decodeIfPresent(Date.self, forKey: .expirationDate)
         status = try container.decodeIfPresent([String].self, forKey: .status) ?? []
@@ -724,9 +839,17 @@ struct DomainOwnership: Codable, Equatable {
     }
 }
 
-struct DiscoveredSubdomain: Codable, Equatable, Hashable, Identifiable {
+struct DiscoveredSubdomain: Codable, Equatable, Hashable, Identifiable, Sendable {
     var id: String { hostname }
     let hostname: String
+    let source: String?
+    let isExtended: Bool
+
+    nonisolated init(hostname: String, source: String? = nil, isExtended: Bool = false) {
+        self.hostname = hostname
+        self.source = source
+        self.isExtended = isExtended
+    }
 }
 
 // MARK: - Email Security Models
@@ -857,9 +980,13 @@ struct HistoryEntry: Identifiable, Codable {
     var emailSecurity: EmailSecurityResult?
     var mtaSts: MTASTSResult?
     var ownership: DomainOwnership?
+    var ownershipHistory: [DomainOwnershipHistoryEvent]
     var ptrRecord: String?
     var redirectChain: [RedirectHop]
     var subdomains: [DiscoveredSubdomain]
+    var extendedSubdomains: [DiscoveredSubdomain]
+    var dnsHistory: [DNSHistoryEvent]
+    var domainPricing: DomainPricingInsight?
     var portScanResults: [PortScanResult]
     var hstsPreloaded: Bool?
     var availabilityResult: DomainAvailabilityResult?
@@ -891,16 +1018,23 @@ struct HistoryEntry: Identifiable, Codable {
     var ipGeolocationError: String?
     var emailSecurityError: String?
     var ownershipError: String?
+    var ownershipHistoryError: String?
     var ptrError: String?
     var redirectChainError: String?
     var subdomainsError: String?
+    var extendedSubdomainsError: String?
+    var dnsHistoryError: String?
+    var domainPricingError: String?
     var portScanError: String?
 
     init(domain: String, timestamp: Date, trackedDomainID: UUID? = nil, note: String? = nil, dnsSections: [DNSSection],
          sslInfo: SSLCertificateInfo?, httpHeaders: [HTTPHeader],
          reachabilityResults: [PortReachability], ipGeolocation: IPGeolocation?,
          emailSecurity: EmailSecurityResult? = nil, mtaSts: MTASTSResult? = nil, ownership: DomainOwnership? = nil,
+         ownershipHistory: [DomainOwnershipHistoryEvent] = [],
          ptrRecord: String? = nil, redirectChain: [RedirectHop] = [], subdomains: [DiscoveredSubdomain] = [],
+         extendedSubdomains: [DiscoveredSubdomain] = [], dnsHistory: [DNSHistoryEvent] = [],
+         domainPricing: DomainPricingInsight? = nil,
          portScanResults: [PortScanResult] = [],
          hstsPreloaded: Bool? = nil, availabilityResult: DomainAvailabilityResult? = nil,
          suggestions: [DomainSuggestionResult] = [], appVersion: String = "2.7.0",
@@ -915,8 +1049,10 @@ struct HistoryEntry: Identifiable, Codable {
          tlsStatusSummary: String? = nil, emailSecuritySummary: String? = nil, httpGradeSummary: String? = nil,
          changeSummary: DomainChangeSummary? = nil, sslError: String? = nil, httpHeadersError: String? = nil,
          reachabilityError: String? = nil, ipGeolocationError: String? = nil,
-         emailSecurityError: String? = nil, ownershipError: String? = nil, ptrError: String? = nil,
-         redirectChainError: String? = nil, subdomainsError: String? = nil, portScanError: String? = nil) {
+         emailSecurityError: String? = nil, ownershipError: String? = nil, ownershipHistoryError: String? = nil,
+         ptrError: String? = nil, redirectChainError: String? = nil, subdomainsError: String? = nil,
+         extendedSubdomainsError: String? = nil, dnsHistoryError: String? = nil,
+         domainPricingError: String? = nil, portScanError: String? = nil) {
         self.domain = domain
         self.timestamp = timestamp
         self.trackedDomainID = trackedDomainID
@@ -929,9 +1065,13 @@ struct HistoryEntry: Identifiable, Codable {
         self.emailSecurity = emailSecurity
         self.mtaSts = mtaSts ?? emailSecurity?.mtaSts
         self.ownership = ownership
+        self.ownershipHistory = ownershipHistory
         self.ptrRecord = ptrRecord
         self.redirectChain = redirectChain
         self.subdomains = subdomains
+        self.extendedSubdomains = extendedSubdomains
+        self.dnsHistory = dnsHistory
+        self.domainPricing = domainPricing
         self.portScanResults = portScanResults
         self.hstsPreloaded = hstsPreloaded
         self.availabilityResult = availabilityResult
@@ -963,9 +1103,13 @@ struct HistoryEntry: Identifiable, Codable {
         self.ipGeolocationError = ipGeolocationError
         self.emailSecurityError = emailSecurityError
         self.ownershipError = ownershipError
+        self.ownershipHistoryError = ownershipHistoryError
         self.ptrError = ptrError
         self.redirectChainError = redirectChainError
         self.subdomainsError = subdomainsError
+        self.extendedSubdomainsError = extendedSubdomainsError
+        self.dnsHistoryError = dnsHistoryError
+        self.domainPricingError = domainPricingError
         self.portScanError = portScanError
     }
 
@@ -984,9 +1128,13 @@ struct HistoryEntry: Identifiable, Codable {
         emailSecurity = try container.decodeIfPresent(EmailSecurityResult.self, forKey: .emailSecurity)
         mtaSts = try container.decodeIfPresent(MTASTSResult.self, forKey: .mtaSts) ?? emailSecurity?.mtaSts
         ownership = try container.decodeIfPresent(DomainOwnership.self, forKey: .ownership)
+        ownershipHistory = try container.decodeIfPresent([DomainOwnershipHistoryEvent].self, forKey: .ownershipHistory) ?? []
         ptrRecord = try container.decodeIfPresent(String.self, forKey: .ptrRecord)
         redirectChain = try container.decodeIfPresent([RedirectHop].self, forKey: .redirectChain) ?? []
         subdomains = try container.decodeIfPresent([DiscoveredSubdomain].self, forKey: .subdomains) ?? []
+        extendedSubdomains = try container.decodeIfPresent([DiscoveredSubdomain].self, forKey: .extendedSubdomains) ?? []
+        dnsHistory = try container.decodeIfPresent([DNSHistoryEvent].self, forKey: .dnsHistory) ?? []
+        domainPricing = try container.decodeIfPresent(DomainPricingInsight.self, forKey: .domainPricing)
         portScanResults = try container.decodeIfPresent([PortScanResult].self, forKey: .portScanResults) ?? []
         hstsPreloaded = try container.decodeIfPresent(Bool.self, forKey: .hstsPreloaded)
         availabilityResult = try container.decodeIfPresent(DomainAvailabilityResult.self, forKey: .availabilityResult)
@@ -1018,9 +1166,13 @@ struct HistoryEntry: Identifiable, Codable {
         ipGeolocationError = try container.decodeIfPresent(String.self, forKey: .ipGeolocationError)
         emailSecurityError = try container.decodeIfPresent(String.self, forKey: .emailSecurityError)
         ownershipError = try container.decodeIfPresent(String.self, forKey: .ownershipError)
+        ownershipHistoryError = try container.decodeIfPresent(String.self, forKey: .ownershipHistoryError)
         ptrError = try container.decodeIfPresent(String.self, forKey: .ptrError)
         redirectChainError = try container.decodeIfPresent(String.self, forKey: .redirectChainError)
         subdomainsError = try container.decodeIfPresent(String.self, forKey: .subdomainsError)
+        extendedSubdomainsError = try container.decodeIfPresent(String.self, forKey: .extendedSubdomainsError)
+        dnsHistoryError = try container.decodeIfPresent(String.self, forKey: .dnsHistoryError)
+        domainPricingError = try container.decodeIfPresent(String.self, forKey: .domainPricingError)
         portScanError = try container.decodeIfPresent(String.self, forKey: .portScanError)
     }
 
diff --git a/DomainDig/PaywallView.swift b/DomainDig/PaywallView.swift
index a21448f..d0293a8 100644
--- a/DomainDig/PaywallView.swift
+++ b/DomainDig/PaywallView.swift
@@ -10,7 +10,7 @@ struct PaywallView: View {
         NavigationStack {
             List {
                 Section {
-                    Text("Pro unlocks automation, larger runs, and export convenience while keeping the core lookup experience free.")
+                    Text("Pro unlocks workflows, scale, and exports. Data+ adds deeper external intelligence with local-first usage credits and no account requirement.")
                         .font(appDensity.font(.body, design: .default))
                         .foregroundStyle(.secondary)
                         .fixedSize(horizontal: false, vertical: true)
@@ -23,6 +23,13 @@ struct PaywallView: View {
                     featureRow("Advanced exports")
                 }
 
+                Section("What Data+ Unlocks") {
+                    featureRow("Ownership history")
+                    featureRow("DNS history")
+                    featureRow("Extended subdomains")
+                    featureRow("External pricing signals")
+                }
+
                 Section("Subscription") {
                     if purchaseService.isLoadingProducts {
                         ProgressView("Loading pricing…")
@@ -116,6 +123,10 @@ struct PaywallView: View {
             return "Pro Monthly"
         case PurchaseService.yearlyProductID:
             return "Pro Yearly"
+        case PurchaseService.dataPlusMonthlyProductID:
+            return "Data+ Monthly"
+        case PurchaseService.dataPlusYearlyProductID:
+            return "Data+ Yearly"
         default:
             return product.displayName
         }
diff --git a/DomainDig/PurchaseService.swift b/DomainDig/PurchaseService.swift
index 3d19165..20ee818 100644
--- a/DomainDig/PurchaseService.swift
+++ b/DomainDig/PurchaseService.swift
@@ -17,7 +17,14 @@ final class PurchaseService {
     static let shared = PurchaseService()
     static let monthlyProductID = "domaindig.pro.monthly"
     static let yearlyProductID = "domaindig.pro.yearly"
-    static let productIDs = [monthlyProductID, yearlyProductID]
+    static let dataPlusMonthlyProductID = "domaindig.dataplus.monthly"
+    static let dataPlusYearlyProductID = "domaindig.dataplus.yearly"
+    static let productIDs = [
+        monthlyProductID,
+        yearlyProductID,
+        dataPlusMonthlyProductID,
+        dataPlusYearlyProductID
+    ]
 
     private static let entitlementCacheKey = "purchase.cachedEntitlement"
 
@@ -52,7 +59,11 @@ final class PurchaseService {
     }
 
     var hasProAccess: Bool {
-        currentTier == .pro
+        currentTier != .free
+    }
+
+    var hasDataPlusAccess: Bool {
+        currentTier == .dataPlus
     }
 
     func refreshProducts() async {
@@ -91,7 +102,7 @@ final class PurchaseService {
             .productID
 
         self.activeProductID = activeProductID
-        currentTier = activeProductID == nil ? .free : .pro
+        currentTier = tier(for: activeProductID)
         persistCurrentEntitlement()
     }
 
@@ -109,7 +120,7 @@ final class PurchaseService {
                 apply(transaction: transaction)
                 await transaction.finish()
                 await refreshEntitlements()
-                statusMessage = "Pro is active."
+                statusMessage = currentTier == .dataPlus ? "Data+ is active." : "Pro is active."
             case .userCancelled:
                 break
             case .pending:
@@ -177,7 +188,7 @@ final class PurchaseService {
         }
 
         activeProductID = transaction.productID
-        currentTier = .pro
+        currentTier = tier(for: transaction.productID)
         persistCurrentEntitlement()
     }
 
@@ -224,11 +235,26 @@ final class PurchaseService {
             return 0
         case Self.yearlyProductID:
             return 1
+        case Self.dataPlusMonthlyProductID:
+            return 2
+        case Self.dataPlusYearlyProductID:
+            return 3
         default:
             return Int.max
         }
     }
 
+    private func tier(for productID: String?) -> FeatureTier {
+        switch productID {
+        case Self.monthlyProductID, Self.yearlyProductID:
+            return .pro
+        case Self.dataPlusMonthlyProductID, Self.dataPlusYearlyProductID:
+            return .dataPlus
+        default:
+            return .free
+        }
+    }
+
     private func storeMessage(for error: Error, fallback: String) -> String {
         if let storeKitError = error as? StoreKitError {
             switch storeKitError {
diff --git a/DomainDig/RDAPService.swift b/DomainDig/RDAPService.swift
index 3556624..e7e2c93 100644
--- a/DomainDig/RDAPService.swift
+++ b/DomainDig/RDAPService.swift
@@ -25,6 +25,7 @@ enum RDAPService {
         case let .success(response):
             let ownership = DomainOwnership(
                 registrar: response.registrarName,
+                registrant: response.registrantName,
                 createdDate: response.createdDate,
                 expirationDate: response.expirationDate,
                 status: response.status,
@@ -113,6 +114,10 @@ private struct RDAPDomainResponse: Decodable, Sendable {
         entities?.first(where: { $0.roles.contains("registrar") })?.bestDisplayName
     }
 
+    var registrantName: String? {
+        entities?.first(where: { $0.roles.contains("registrant") })?.bestDisplayName
+    }
+
     var createdDate: Date? {
         eventDate(for: ["registration", "registered"])
     }
diff --git a/DomainDig/SubdomainDiscoveryService.swift b/DomainDig/SubdomainDiscoveryService.swift
index 4e25cf0..7339648 100644
--- a/DomainDig/SubdomainDiscoveryService.swift
+++ b/DomainDig/SubdomainDiscoveryService.swift
@@ -59,7 +59,7 @@ enum SubdomainDiscoveryService {
                 guard seen.insert(sanitized).inserted else {
                     continue
                 }
-                results.append(DiscoveredSubdomain(hostname: sanitized))
+                results.append(DiscoveredSubdomain(hostname: sanitized, source: "crt.sh"))
                 if results.count == limit {
                     return results
                 }
diff --git a/DomainDig/UsageCreditService.swift b/DomainDig/UsageCreditService.swift
new file mode 100644
index 0000000..48f40be
--- /dev/null
+++ b/DomainDig/UsageCreditService.swift
@@ -0,0 +1,73 @@
+import Foundation
+
+actor UsageCreditService {
+    static let shared = UsageCreditService()
+
+    private struct CreditLedger: Codable {
+        let appVersion: String
+        var remainingByFeature: [UsageCreditFeature: Int]
+    }
+
+    private let storageKey = "usageCredits.ledger"
+    private var ledger: CreditLedger
+
+    init(defaults: UserDefaults = .standard) {
+        if let data = defaults.data(forKey: storageKey),
+           let decoded = try? JSONDecoder().decode(CreditLedger.self, from: data),
+           decoded.appVersion == AppVersion.current {
+            ledger = decoded
+        } else {
+            ledger = Self.makeLedger()
+            if let data = try? JSONEncoder().encode(ledger) {
+                defaults.set(data, forKey: storageKey)
+            }
+        }
+    }
+
+    func status(for feature: UsageCreditFeature) -> UsageCreditStatus {
+        let total = feature.defaultAllowance
+        let remaining = ledger.remainingByFeature[feature] ?? total
+        return UsageCreditStatus(
+            feature: feature,
+            remaining: remaining,
+            total: total,
+            resetContext: "Resets with app version \(ledger.appVersion)"
+        )
+    }
+
+    func allStatuses() -> [UsageCreditStatus] {
+        UsageCreditFeature.allCases.map { status(for: $0) }
+    }
+
+    func canUse(_ feature: UsageCreditFeature) -> Bool {
+        status(for: feature).remaining > 0
+    }
+
+    @discardableResult
+    func consume(_ feature: UsageCreditFeature) -> UsageCreditStatus {
+        let current = ledger.remainingByFeature[feature] ?? feature.defaultAllowance
+        ledger.remainingByFeature[feature] = max(0, current - 1)
+        persist()
+        return status(for: feature)
+    }
+
+    func resetForCurrentVersion() {
+        ledger = Self.makeLedger()
+        persist()
+    }
+
+    private static func makeLedger() -> CreditLedger {
+        CreditLedger(
+            appVersion: AppVersion.current,
+            remainingByFeature: Dictionary(
+                uniqueKeysWithValues: UsageCreditFeature.allCases.map { ($0, $0.defaultAllowance) }
+            )
+        )
+    }
+
+    private func persist(defaults: UserDefaults = .standard) {
+        if let data = try? JSONEncoder().encode(ledger) {
+            defaults.set(data, forKey: storageKey)
+        }
+    }
+}
diff --git a/DomainDigCLI.swift b/DomainDigCLI.swift
index d0a4d86..a6d2cf3 100644
--- a/DomainDigCLI.swift
+++ b/DomainDigCLI.swift
@@ -6,12 +6,17 @@ struct DomainDigCLI {
         let arguments = Array(CommandLine.arguments.dropFirst())
 
         guard let command = CommandLine.arguments.first else {
-            fputs("usage: domaindig <domain> [--json]\n", stderr)
+            fputs("usage: domaindig <domain> [--json] [--ownership-history] [--dns-history] [--extended-subdomains] [--pricing] [--show-usage]\n", stderr)
             Foundation.exit(1)
         }
         _ = command
 
         let wantsJSON = arguments.contains("--json") || arguments.contains("-j")
+        let wantsOwnershipHistory = arguments.contains("--ownership-history")
+        let wantsDNSHistory = arguments.contains("--dns-history")
+        let wantsExtendedSubdomains = arguments.contains("--extended-subdomains")
+        let wantsPricing = arguments.contains("--pricing")
+        let wantsUsage = arguments.contains("--show-usage")
         let domains = arguments.filter { !$0.hasPrefix("-") }
 
         let requestedDomains = domains
@@ -19,18 +24,29 @@ struct DomainDigCLI {
             .filter { !$0.isEmpty }
 
         guard !requestedDomains.isEmpty else {
-            fputs("usage: domaindig <domain> [--json]\n", stderr)
+            fputs("usage: domaindig <domain> [--json] [--ownership-history] [--dns-history] [--extended-subdomains] [--pricing] [--show-usage]\n", stderr)
             Foundation.exit(1)
         }
 
         let inspectionService = DomainInspectionService()
+        let reportBuilder = DomainReportBuilder()
         var reports: [DomainReport] = []
         var seen = Set<String>()
+        var usageImpact: [String] = []
 
         for domain in requestedDomains {
             let normalizedDomain = domain.lowercased()
             guard seen.insert(normalizedDomain).inserted else { continue }
-            reports.append(await inspectionService.inspect(domain: domain))
+            let snapshot = await inspectionService.inspectSnapshot(domain: domain)
+            let enrichedSnapshot = await enrichSnapshot(
+                snapshot,
+                wantsOwnershipHistory: wantsOwnershipHistory,
+                wantsDNSHistory: wantsDNSHistory,
+                wantsExtendedSubdomains: wantsExtendedSubdomains,
+                wantsPricing: wantsPricing,
+                usageImpact: &usageImpact
+            )
+            reports.append(reportBuilder.build(from: enrichedSnapshot))
         }
 
         do {
@@ -47,6 +63,9 @@ struct DomainDigCLI {
                     title: "DomainDig Batch Report"
                 )
             }
+            if wantsUsage, !usageImpact.isEmpty {
+                FileHandle.standardError.write(Data(("Data+ usage impact: " + usageImpact.joined(separator: ", ") + "\n").utf8))
+            }
             FileHandle.standardOutput.write(data)
             if data.last != 0x0A {
                 FileHandle.standardOutput.write(Data([0x0A]))
@@ -56,4 +75,187 @@ struct DomainDigCLI {
             Foundation.exit(1)
         }
     }
+
+    private static func enrichSnapshot(
+        _ snapshot: LookupSnapshot,
+        wantsOwnershipHistory: Bool,
+        wantsDNSHistory: Bool,
+        wantsExtendedSubdomains: Bool,
+        wantsPricing: Bool,
+        usageImpact: inout [String]
+    ) async -> LookupSnapshot {
+        guard FeatureAccessService.currentTier == .dataPlus else {
+            return snapshot
+        }
+
+        let historyEntries = loadHistoryEntries()
+        var ownershipHistory = snapshot.ownershipHistory
+        var ownershipHistoryError = snapshot.ownershipHistoryError
+        var dnsHistory = snapshot.dnsHistory
+        var dnsHistoryError = snapshot.dnsHistoryError
+        var extendedSubdomains = snapshot.extendedSubdomains
+        var extendedSubdomainsError = snapshot.extendedSubdomainsError
+        var domainPricing = snapshot.domainPricing
+        var domainPricingError = snapshot.domainPricingError
+
+        if wantsOwnershipHistory,
+           await UsageCreditService.shared.canUse(.ownershipHistory) {
+            let outcome = await ExternalDataService.shared.ownershipHistory(
+                domain: snapshot.domain,
+                currentOwnership: snapshot.ownership,
+                historyEntries: historyEntries
+            )
+            switch outcome.value {
+            case let .success(events):
+                ownershipHistory = events
+                ownershipHistoryError = nil
+                if outcome.source != .cached {
+                    _ = await UsageCreditService.shared.consume(.ownershipHistory)
+                    usageImpact.append("ownership history -1")
+                }
+            case let .empty(message):
+                ownershipHistoryError = message
+                if outcome.source != .cached {
+                    _ = await UsageCreditService.shared.consume(.ownershipHistory)
+                    usageImpact.append("ownership history -1")
+                }
+            case let .error(message):
+                ownershipHistoryError = message
+            }
+        }
+
+        if wantsDNSHistory,
+           await UsageCreditService.shared.canUse(.dnsHistory) {
+            let outcome = await ExternalDataService.shared.dnsHistory(
+                domain: snapshot.domain,
+                dnsSections: snapshot.dnsSections,
+                historyEntries: historyEntries
+            )
+            switch outcome.value {
+            case let .success(events):
+                dnsHistory = events
+                dnsHistoryError = nil
+                if outcome.source != .cached {
+                    _ = await UsageCreditService.shared.consume(.dnsHistory)
+                    usageImpact.append("dns history -1")
+                }
+            case let .empty(message):
+                dnsHistoryError = message
+                if outcome.source != .cached {
+                    _ = await UsageCreditService.shared.consume(.dnsHistory)
+                    usageImpact.append("dns history -1")
+                }
+            case let .error(message):
+                dnsHistoryError = message
+            }
+        }
+
+        if wantsExtendedSubdomains,
+           await UsageCreditService.shared.canUse(.extendedSubdomains) {
+            let outcome = await ExternalDataService.shared.extendedSubdomains(
+                domain: snapshot.domain,
+                existing: snapshot.subdomains
+            )
+            switch outcome.value {
+            case let .success(results):
+                extendedSubdomains = results
+                extendedSubdomainsError = nil
+                if outcome.source != .cached {
+                    _ = await UsageCreditService.shared.consume(.extendedSubdomains)
+                    usageImpact.append("extended subdomains -1")
+                }
+            case let .empty(message):
+                extendedSubdomainsError = message
+                if outcome.source != .cached {
+                    _ = await UsageCreditService.shared.consume(.extendedSubdomains)
+                    usageImpact.append("extended subdomains -1")
+                }
+            case let .error(message):
+                extendedSubdomainsError = message
+            }
+        }
+
+        if wantsPricing {
+            let outcome = await ExternalDataService.shared.pricing(domain: snapshot.domain)
+            switch outcome.value {
+            case let .success(pricing):
+                domainPricing = pricing
+                domainPricingError = nil
+            case let .empty(message), let .error(message):
+                domainPricingError = message
+            }
+        }
+
+        return LookupSnapshot(
+            historyEntryID: snapshot.historyEntryID,
+            domain: snapshot.domain,
+            timestamp: snapshot.timestamp,
+            trackedDomainID: snapshot.trackedDomainID,
+            note: snapshot.note,
+            appVersion: snapshot.appVersion,
+            resolverDisplayName: snapshot.resolverDisplayName,
+            resolverURLString: snapshot.resolverURLString,
+            dataSources: snapshot.dataSources,
+            provenanceBySection: snapshot.provenanceBySection,
+            availabilityConfidence: snapshot.availabilityConfidence,
+            ownershipConfidence: snapshot.ownershipConfidence,
+            subdomainConfidence: snapshot.subdomainConfidence,
+            emailSecurityConfidence: snapshot.emailSecurityConfidence,
+            geolocationConfidence: snapshot.geolocationConfidence,
+            errorDetails: snapshot.errorDetails,
+            isPartialSnapshot: snapshot.isPartialSnapshot,
+            validationIssues: snapshot.validationIssues,
+            totalLookupDurationMs: snapshot.totalLookupDurationMs,
+            dnsSections: snapshot.dnsSections,
+            dnsError: snapshot.dnsError,
+            availabilityResult: snapshot.availabilityResult,
+            suggestions: snapshot.suggestions,
+            sslInfo: snapshot.sslInfo,
+            sslError: snapshot.sslError,
+            hstsPreloaded: snapshot.hstsPreloaded,
+            httpHeaders: snapshot.httpHeaders,
+            httpSecurityGrade: snapshot.httpSecurityGrade,
+            httpStatusCode: snapshot.httpStatusCode,
+            httpResponseTimeMs: snapshot.httpResponseTimeMs,
+            httpProtocol: snapshot.httpProtocol,
+            http3Advertised: snapshot.http3Advertised,
+            httpHeadersError: snapshot.httpHeadersError,
+            reachabilityResults: snapshot.reachabilityResults,
+            reachabilityError: snapshot.reachabilityError,
+            ipGeolocation: snapshot.ipGeolocation,
+            ipGeolocationError: snapshot.ipGeolocationError,
+            emailSecurity: snapshot.emailSecurity,
+            emailSecurityError: snapshot.emailSecurityError,
+            ownership: snapshot.ownership,
+            ownershipError: snapshot.ownershipError,
+            ownershipHistory: ownershipHistory,
+            ownershipHistoryError: ownershipHistoryError,
+            ptrRecord: snapshot.ptrRecord,
+            ptrError: snapshot.ptrError,
+            redirectChain: snapshot.redirectChain,
+            redirectChainError: snapshot.redirectChainError,
+            subdomains: snapshot.subdomains,
+            subdomainsError: snapshot.subdomainsError,
+            extendedSubdomains: extendedSubdomains,
+            extendedSubdomainsError: extendedSubdomainsError,
+            dnsHistory: dnsHistory,
+            dnsHistoryError: dnsHistoryError,
+            domainPricing: domainPricing,
+            domainPricingError: domainPricingError,
+            portScanResults: snapshot.portScanResults,
+            portScanError: snapshot.portScanError,
+            changeSummary: snapshot.changeSummary,
+            resultSource: snapshot.resultSource,
+            cachedSections: snapshot.cachedSections,
+            statusMessage: snapshot.statusMessage
+        )
+    }
+
+    private static func loadHistoryEntries() -> [HistoryEntry] {
+        guard let data = UserDefaults.standard.data(forKey: "lookupHistory"),
+              let entries = try? JSONDecoder().decode([HistoryEntry].self, from: data) else {
+            return []
+        }
+        return entries
+    }
 }
diff --git a/DomainInspectionService.swift b/DomainInspectionService.swift
index b906b83..a4a1172 100644
--- a/DomainInspectionService.swift
+++ b/DomainInspectionService.swift
@@ -313,12 +313,20 @@ struct DomainInspectionService {
             emailSecurityError: emailSecurity.message,
             ownership: ownership.value,
             ownershipError: ownership.message,
+            ownershipHistory: [],
+            ownershipHistoryError: nil,
             ptrRecord: ptrRecord.value,
             ptrError: ptrRecord.message,
             redirectChain: redirectChain.value,
             redirectChainError: redirectChain.message,
             subdomains: subdomains.value,
             subdomainsError: subdomains.message,
+            extendedSubdomains: [],
+            extendedSubdomainsError: nil,
+            dnsHistory: [],
+            dnsHistoryError: nil,
+            domainPricing: nil,
+            domainPricingError: nil,
             portScanResults: portScanResults.value,
             portScanError: portScanResults.message,
             changeSummary: nil,
diff --git a/DomainReportBuilder.swift b/DomainReportBuilder.swift
index 0f42519..3ed88ac 100644
--- a/DomainReportBuilder.swift
+++ b/DomainReportBuilder.swift
@@ -21,11 +21,15 @@ struct DomainReport: Codable {
     let emailConfidence: ConfidenceLevel?
     let geolocationConfidence: ConfidenceLevel?
     let ownership: DomainOwnership?
+    let ownershipHistory: [DomainOwnershipHistoryEvent]
     let dns: DNSResultSummary
     let web: WebResultSummary
     let email: EmailSecuritySummary
     let network: NetworkSummary
     let subdomains: [String]
+    let extendedSubdomains: [String]
+    let dnsHistory: [DNSHistoryEvent]
+    let domainPricing: DomainPricingInsight?
     let subdomainGroups: [SubdomainGroup]
     let riskAssessment: DomainRiskAssessment
     let insights: [String]
@@ -164,6 +168,7 @@ struct DomainReportBuilder {
             emailConfidence: snapshot.emailSecurityConfidence,
             geolocationConfidence: snapshot.geolocationConfidence,
             ownership: snapshot.ownership,
+            ownershipHistory: snapshot.ownershipHistory,
             dns: DNSResultSummary(
                 resolverDisplayName: snapshot.resolverDisplayName,
                 resolverURLString: snapshot.resolverURLString,
@@ -216,13 +221,16 @@ struct DomainReportBuilder {
                 portScanError: snapshot.portScanError
             ),
             subdomains: snapshot.subdomains.map(\.hostname),
+            extendedSubdomains: snapshot.extendedSubdomains.map(\.hostname),
+            dnsHistory: snapshot.dnsHistory,
+            domainPricing: snapshot.domainPricing,
             subdomainGroups: analysis.subdomainGroups,
             riskAssessment: analysis.riskAssessment,
             insights: analysis.insights,
             changeSummary: changeSummary,
             workflowContext: workflowContext,
             metadata: DomainReportMetadata(
-                schemaVersion: "3.0.0",
+                schemaVersion: "3.2.0",
                 resolverDisplayName: snapshot.resolverDisplayName,
                 resolverURLString: snapshot.resolverURLString,
                 appVersion: snapshot.appVersion,
diff --git a/DomainReportExporter.swift b/DomainReportExporter.swift
index b7d4606..4fa7470 100644
--- a/DomainReportExporter.swift
+++ b/DomainReportExporter.swift
@@ -67,7 +67,9 @@ enum DomainReportExporter {
                 "TLS Status: \(report.web.tlsStatus)",
                 "HTTP: \(httpSummary(for: report))",
                 "Email: \(report.email.summary)",
-                "Subdomains: \(report.subdomains.count)"
+                "Subdomains: \(report.subdomains.count)",
+                "Extended Subdomains: \(report.extendedSubdomains.count)",
+                "External Price: \(report.domainPricing?.estimatedPrice ?? "Unavailable")"
             ]
         }
 
@@ -97,6 +99,7 @@ enum DomainReportExporter {
                 "Confidence: \(report.ownershipConfidence?.title ?? "N/A")",
                 "Created: \(ownershipDateLabel(report.ownership?.createdDate))",
                 "Expires: \(ownershipDateLabel(report.ownership?.expirationDate))",
+                "Registrant: \(report.ownership?.registrant ?? "Unavailable")",
                 "Nameservers: \(joined(report.ownership?.nameservers) ?? "Unavailable")",
                 "Status: \(joined(report.ownership?.status) ?? "Unavailable")",
                 "Abuse Contact: \(report.ownership?.abuseEmail ?? "Unavailable")"
@@ -112,6 +115,20 @@ enum DomainReportExporter {
             return ownershipLines
         }
 
+        appendSection("Ownership History", to: &lines) {
+            guard !report.ownershipHistory.isEmpty else {
+                return ["No ownership history available"]
+            }
+
+            return report.ownershipHistory.map { event in
+                [
+                    textDateFormatter.string(from: event.date),
+                    event.summary,
+                    "source=\(event.source)"
+                ].joined(separator: " | ")
+            }
+        }
+
         appendSection("DNS", to: &lines) {
             var dnsLines = [
                 "Lookup Duration: \(durationLabel(report.dns.lookupDurationMs))",
@@ -139,6 +156,22 @@ enum DomainReportExporter {
             return dnsLines
         }
 
+        appendSection("DNS History", to: &lines) {
+            guard !report.dnsHistory.isEmpty else {
+                return ["No DNS history available"]
+            }
+
+            return report.dnsHistory.map { event in
+                [
+                    textDateFormatter.string(from: event.date),
+                    event.summary,
+                    "A=\(event.aRecords.joined(separator: " | ").nilIfEmpty ?? "-")",
+                    "NS=\(event.nameservers.joined(separator: " | ").nilIfEmpty ?? "-")",
+                    "source=\(event.source)"
+                ].joined(separator: " | ")
+            }
+        }
+
         appendSection("Web", to: &lines) {
             var webLines = [
                 "TLS Status: \(report.web.tlsStatus)",
@@ -254,9 +287,28 @@ enum DomainReportExporter {
                 values.append("Groups: \(report.subdomainGroups.map { "\($0.label): \($0.subdomains.count)" }.joined(separator: " | "))")
             }
             values.append(contentsOf: report.subdomains.map { "- \($0)" })
+            if !report.extendedSubdomains.isEmpty {
+                values.append("Extended:")
+                values.append(contentsOf: report.extendedSubdomains.map { "- \($0)" })
+            }
             return values
         }
 
+        appendSection("Pricing", to: &lines) {
+            guard let pricing = report.domainPricing else {
+                return ["External pricing unavailable"]
+            }
+
+            return [
+                "Estimated Price: \(pricing.estimatedPrice ?? "Unavailable")",
+                "Premium: \(pricing.premiumIndicator == true ? "Yes" : "No")",
+                "Resale: \(pricing.resaleSignal ?? "Unavailable")",
+                "Auction: \(pricing.auctionSignal ?? "Unavailable")",
+                "Source: \(pricing.source)",
+                "Collected: \(textDateFormatter.string(from: pricing.collectedAt))"
+            ]
+        }
+
         appendSection("Changes", to: &lines) {
             guard let changeSummary = report.changeSummary else {
                 return ["No comparison available"]
@@ -342,9 +394,18 @@ enum DomainReportExporter {
             "email_grade",
             "email_confidence",
             "subdomain_count",
+            "extended_subdomain_count",
             "subdomain_groups",
             "subdomain_confidence",
             "subdomains",
+            "extended_subdomains",
+            "ownership_history",
+            "dns_history",
+            "pricing_estimated",
+            "pricing_premium",
+            "pricing_resale_signal",
+            "pricing_auction_signal",
+            "pricing_source",
             "open_ports",
             "reachability_summary",
             "geolocation_summary",
@@ -368,12 +429,15 @@ enum DomainReportExporter {
             let httpStatus = report.web.statusCode.map(String.init) ?? ""
             let subdomainCount = String(report.subdomains.count)
             let subdomains = report.subdomains.joined(separator: " | ")
+            let extendedSubdomains = report.extendedSubdomains.joined(separator: " | ")
             let openPorts = report.network.openPorts.map(String.init).joined(separator: " | ")
             let riskFactors = report.riskAssessment.factors.map(\.description).joined(separator: " | ")
             let insights = report.insights.joined(separator: " | ")
             let dnsPatterns = report.dns.patternSummary.patterns.joined(separator: " | ")
             let tlsHighlights = report.web.tlsHighlights.joined(separator: " | ")
             let subdomainGroups = report.subdomainGroups.map { "\($0.label):\($0.subdomains.count)" }.joined(separator: " | ")
+            let ownershipHistory = report.ownershipHistory.map { "\($0.date.ISO8601Format()) \($0.summary)" }.joined(separator: " | ")
+            let dnsHistory = report.dnsHistory.map { "\($0.date.ISO8601Format()) \($0.summary)" }.joined(separator: " | ")
 
             return [
                 report.domain,
@@ -407,9 +471,18 @@ enum DomainReportExporter {
                 report.email.grade?.rawValue ?? "",
                 report.emailConfidence?.rawValue ?? "",
                 subdomainCount,
+                String(report.extendedSubdomains.count),
                 subdomainGroups,
                 report.subdomainConfidence?.rawValue ?? "",
                 subdomains,
+                extendedSubdomains,
+                ownershipHistory,
+                dnsHistory,
+                report.domainPricing?.estimatedPrice ?? "",
+                report.domainPricing?.premiumIndicator == true ? "true" : "false",
+                report.domainPricing?.resaleSignal ?? "",
+                report.domainPricing?.auctionSignal ?? "",
+                report.domainPricing?.source ?? "",
                 openPorts,
                 report.network.reachabilitySummary,
                 report.network.geolocationSummary,
diff --git a/LookupSnapshot.swift b/LookupSnapshot.swift
index f51f546..a817805 100644
--- a/LookupSnapshot.swift
+++ b/LookupSnapshot.swift
@@ -42,12 +42,20 @@ struct LookupSnapshot {
     let emailSecurityError: String?
     let ownership: DomainOwnership?
     let ownershipError: String?
+    let ownershipHistory: [DomainOwnershipHistoryEvent]
+    let ownershipHistoryError: String?
     let ptrRecord: String?
     let ptrError: String?
     let redirectChain: [RedirectHop]
     let redirectChainError: String?
     let subdomains: [DiscoveredSubdomain]
     let subdomainsError: String?
+    let extendedSubdomains: [DiscoveredSubdomain]
+    let extendedSubdomainsError: String?
+    let dnsHistory: [DNSHistoryEvent]
+    let dnsHistoryError: String?
+    let domainPricing: DomainPricingInsight?
+    let domainPricingError: String?
     let portScanResults: [PortScanResult]
     let portScanError: String?
     let changeSummary: DomainChangeSummary?
@@ -104,12 +112,20 @@ extension HistoryEntry {
             emailSecurityError: emailSecurityError,
             ownership: ownership,
             ownershipError: ownershipError,
+            ownershipHistory: ownershipHistory,
+            ownershipHistoryError: ownershipHistoryError,
             ptrRecord: ptrRecord,
             ptrError: ptrError,
             redirectChain: redirectChain,
             redirectChainError: redirectChainError,
             subdomains: subdomains,
             subdomainsError: subdomainsError,
+            extendedSubdomains: extendedSubdomains,
+            extendedSubdomainsError: extendedSubdomainsError,
+            dnsHistory: dnsHistory,
+            dnsHistoryError: dnsHistoryError,
+            domainPricing: domainPricing,
+            domainPricingError: domainPricingError,
             portScanResults: portScanResults,
             portScanError: portScanError,
             changeSummary: changeSummary,