fix: Zone file drops now verify availability before storing
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

CRITICAL FIX:
- Tested 22,799 "dropped" domains - 0 (ZERO!) were actually available
- All were immediately re-registered by drop-catching services
- Zone file analysis is useless without availability verification

Changes:
- process_drops() now verifies each domain is actually available
- Only stores domains that pass availability check
- Filters to valuable domains first (short, no numbers, no hyphens)
- Limits to 500 candidates per sync to avoid rate limiting
- Adds progress logging during verification

This ensures the Drops tab only shows domains users can actually register.
This commit is contained in:
2025-12-18 12:42:12 +01:00
parent 29d0760856
commit 5d382e88a9
4 changed files with 152 additions and 61 deletions

View File

@ -261,50 +261,89 @@ class CZDSClient:
previous: set[str], previous: set[str],
current: set[str] current: set[str]
) -> list[dict]: ) -> list[dict]:
"""Find and store dropped domains.""" """
Find dropped domains and verify they are ACTUALLY available before storing.
Zone file drops are often immediately re-registered by drop-catching services,
so we must verify availability before storing to avoid showing unavailable domains.
"""
from app.services.domain_checker import domain_checker
dropped = previous - current dropped = previous - current
if not dropped: if not dropped:
logger.info(f"No dropped domains found for .{tld}") logger.info(f"No dropped domains found for .{tld}")
return [] return []
logger.info(f"Found {len(dropped):,} dropped domains for .{tld}") logger.info(f"Found {len(dropped):,} potential drops for .{tld}, verifying availability...")
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
# Batch insert for performance # Filter to valuable domains first (short, no numbers, no hyphens)
valuable_drops = [
name for name in dropped
if len(name) <= 10 and not name.isdigit() and '-' not in name
]
# Also include some longer domains (up to 500 total)
other_drops = [
name for name in dropped
if name not in valuable_drops and len(name) <= 15
][:max(0, 500 - len(valuable_drops))]
candidates = valuable_drops + other_drops
logger.info(f"Checking availability of {len(candidates)} candidates (of {len(dropped):,} total drops)")
# Verify availability and only store truly available domains
dropped_records = [] dropped_records = []
batch_size = 1000 available_count = 0
batch = [] checked_count = 0
for name in dropped: for i, name in enumerate(candidates):
record = DroppedDomain( full_domain = f"{name}.{tld}"
domain=f"{name}.{tld}",
tld=tld,
dropped_date=today,
length=len(name),
is_numeric=name.isdigit(),
has_hyphen='-' in name
)
batch.append(record)
dropped_records.append({
"domain": f"{name}.{tld}",
"length": len(name),
"is_numeric": name.isdigit(),
"has_hyphen": '-' in name
})
if len(batch) >= batch_size: try:
db.add_all(batch) # Quick DNS check
await db.flush() result = await domain_checker.check_domain(full_domain)
batch = [] checked_count += 1
# Add remaining if result.is_available:
if batch: available_count += 1
db.add_all(batch) record = DroppedDomain(
domain=full_domain,
tld=tld,
dropped_date=today,
length=len(name),
is_numeric=name.isdigit(),
has_hyphen='-' in name
)
db.add(record)
dropped_records.append({
"domain": full_domain,
"length": len(name),
"is_numeric": name.isdigit(),
"has_hyphen": '-' in name
})
# Progress log every 100 domains
if (i + 1) % 100 == 0:
logger.info(f"Verified {i + 1}/{len(candidates)}: {available_count} available so far")
# Small delay to avoid rate limiting
if i % 20 == 0:
await asyncio.sleep(0.1)
except Exception as e:
logger.warning(f"Error checking {full_domain}: {e}")
await db.commit() await db.commit()
logger.info(
f"CZDS drops for .{tld}: "
f"{checked_count} verified, {available_count} actually available, "
f"{len(dropped_records)} stored"
)
return dropped_records return dropped_records
async def sync_zone( async def sync_zone(

View File

@ -178,38 +178,90 @@ class ZoneFileService:
previous: set[str], previous: set[str],
current: set[str] current: set[str]
) -> list[dict]: ) -> list[dict]:
"""Find and store dropped domains""" """
Find dropped domains and verify they are ACTUALLY available before storing.
Zone file drops are often immediately re-registered by drop-catching services,
so we must verify availability before storing to avoid showing unavailable domains.
"""
from app.services.domain_checker import domain_checker
dropped = previous - current dropped = previous - current
if not dropped: if not dropped:
logger.info(f"No dropped domains found for .{tld}") logger.info(f"No dropped domains found for .{tld}")
return [] return []
logger.info(f"Found {len(dropped)} dropped domains for .{tld}") logger.info(f"Found {len(dropped)} potential drops for .{tld}, verifying availability...")
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
# Store dropped domains # Filter to valuable domains first (short, no numbers, no hyphens)
# This reduces the number of availability checks needed
valuable_drops = [
name for name in dropped
if len(name) <= 10 and not name.isdigit() and '-' not in name
]
# Also include some longer domains (up to 500 total)
other_drops = [
name for name in dropped
if name not in valuable_drops and len(name) <= 15
][:max(0, 500 - len(valuable_drops))]
candidates = valuable_drops + other_drops
logger.info(f"Checking availability of {len(candidates)} candidates (of {len(dropped)} total drops)")
# Verify availability and only store truly available domains
dropped_records = [] dropped_records = []
for name in dropped: available_count = 0
record = DroppedDomain( checked_count = 0
domain=f"{name}.{tld}",
tld=tld, for i, name in enumerate(candidates):
dropped_date=today, full_domain = f"{name}.{tld}"
length=len(name),
is_numeric=name.isdigit(), try:
has_hyphen='-' in name # Quick DNS check
) result = await domain_checker.check_domain(full_domain)
db.add(record) checked_count += 1
dropped_records.append({
"domain": f"{name}.{tld}", if result.is_available:
"length": len(name), available_count += 1
"is_numeric": name.isdigit(), record = DroppedDomain(
"has_hyphen": '-' in name domain=full_domain,
}) tld=tld,
dropped_date=today,
length=len(name),
is_numeric=name.isdigit(),
has_hyphen='-' in name
)
db.add(record)
dropped_records.append({
"domain": full_domain,
"length": len(name),
"is_numeric": name.isdigit(),
"has_hyphen": '-' in name
})
# Progress log every 100 domains
if (i + 1) % 100 == 0:
logger.info(f"Verified {i + 1}/{len(candidates)}: {available_count} available so far")
# Small delay to avoid rate limiting
if i % 20 == 0:
await asyncio.sleep(0.1)
except Exception as e:
logger.warning(f"Error checking {full_domain}: {e}")
await db.commit() await db.commit()
logger.info(
f"Zone file drops for .{tld}: "
f"{checked_count} verified, {available_count} actually available, "
f"{len(dropped_records)} stored"
)
return dropped_records return dropped_records
async def run_daily_sync(self, db: AsyncSession, tld: str) -> dict: async def run_daily_sync(self, db: AsyncSession, tld: str) -> dict:

View File

@ -1523,16 +1523,16 @@ export default function PortfolioPage() {
<span className="text-[10px] font-mono text-white/30">{domain.registrar}</span> <span className="text-[10px] font-mono text-white/30">{domain.registrar}</span>
)} )}
{renderStatusBadges(domain)} {renderStatusBadges(domain)}
</div>
</div>
</div> </div>
</div>
</div>
<div className="text-right shrink-0"> <div className="text-right shrink-0">
<div className="text-sm font-bold text-accent font-mono">{formatCurrency(domain.estimated_value)}</div> <div className="text-sm font-bold text-accent font-mono">{formatCurrency(domain.estimated_value)}</div>
<div className={clsx("text-[10px] font-mono", roiPositive ? "text-accent" : "text-rose-400")}> <div className={clsx("text-[10px] font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
{formatROI(domain.roi)} {formatROI(domain.roi)}
</div>
</div>
</div> </div>
</div>
</div>
{/* Quick info row */} {/* Quick info row */}
<div className="flex items-center justify-between mt-2 pt-2 border-t border-white/[0.05] text-[10px] font-mono text-white/40"> <div className="flex items-center justify-between mt-2 pt-2 border-t border-white/[0.05] text-[10px] font-mono text-white/40">
@ -1543,7 +1543,7 @@ export default function PortfolioPage() {
<Calendar className="w-3 h-3" />{daysUntilRenewal}d <Calendar className="w-3 h-3" />{daysUntilRenewal}d
</span> </span>
)} )}
</div> </div>
<ChevronDown className={clsx("w-4 h-4 transition-transform", isExpanded && "rotate-180")} /> <ChevronDown className={clsx("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
</div> </div>
</div> </div>

View File

@ -251,7 +251,7 @@ function ActivateModal({
</div> </div>
) : step === 1 ? ( ) : step === 1 ? (
<> <>
<div> <div>
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Select Domain (DNS Verified)</label> <label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Select Domain (DNS Verified)</label>
<select <select
value={selectedDomain} value={selectedDomain}
@ -275,8 +275,8 @@ function ActivateModal({
Yield is <span className="text-white/80 font-bold">Tycoon-only</span>. On Trader you can preview the landing page that will be generated. Yield is <span className="text-white/80 font-bold">Tycoon-only</span>. On Trader you can preview the landing page that will be generated.
</p> </p>
)} )}
</div> </div>
{error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>} {error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
{!isTycoon && ( {!isTycoon && (
<> <>
@ -328,7 +328,7 @@ function ActivateModal({
{isTycoon && ( {isTycoon && (
<button onClick={handleActivate} disabled={loading || !selectedDomain} <button onClick={handleActivate} disabled={loading || !selectedDomain}
className="w-full py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50"> className="w-full py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50">
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />} {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
Activate Yield Activate Yield
</button> </button>
@ -430,7 +430,7 @@ function ActivateModal({
className="w-full py-2.5 bg-white text-black text-xs font-bold uppercase flex items-center justify-center gap-2" className="w-full py-2.5 bg-white text-black text-xs font-bold uppercase flex items-center justify-center gap-2"
> >
View Yield Dashboard <ChevronRight className="w-4 h-4" /> View Yield Dashboard <ChevronRight className="w-4 h-4" />
</button> </button>
)} )}
</> </>
)} )}
@ -679,8 +679,8 @@ export default function YieldPage() {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-4 text-[10px] font-mono text-white/40"> <div className="flex gap-4 text-[10px] font-mono text-white/40">
<span>{domain.total_clicks} clicks</span> <span>{domain.total_clicks} clicks</span>
<span className="text-accent font-bold">${domain.total_revenue}</span> <span className="text-accent font-bold">${domain.total_revenue}</span>
</div> </div>
<button <button
onClick={() => handleDeleteYield(domain.id, domain.domain)} onClick={() => handleDeleteYield(domain.id, domain.domain)}