Dating App Safety
Protect users from romance scams, catfishing, and phishing links in dating platforms.
Romance scams cost Americans over $1.3 billion annually, making them the most financially devastating form of consumer fraud. Dating platforms are the primary hunting ground. Scammers create fake profiles, build emotional connections, then extract money through fake emergencies, investment schemes, or phishing links. The ScamVerify™ API helps platforms detect these threats at the moment they escalate.
Where Scams Happen on Dating Platforms
Profile Verification Gaps
Scammers register with disposable VoIP numbers that pass basic OTP verification. They cycle through numbers as accounts get banned, creating new profiles within minutes.
Phone Number Exchange
The critical escalation point. When a scammer convinces a victim to move off-platform and share phone numbers, the platform loses visibility. Scanning shared numbers in messages catches known scam numbers before victims make contact.
Malicious Links in Messages
Phishing URLs disguised as "photo albums," "video calls," or "verification links" are the most common attack vector in dating app DMs. These lead to credential harvesting pages, malware downloads, or fake identity verification sites that steal personal information.
How ScamVerify™ Helps
Integrate ScamVerify™ at three points in the user journey:
- Registration: Check phone numbers at signup to block disposable VoIP numbers
- Message scanning: Detect phone numbers and URLs shared in conversations
- Report verification: When users report suspicious profiles, verify the reported contact details
Code Example: Message Content Scanning
class DatingAppSafetyScanner {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://scamverify.ai/api/v1';
}
async callApi(endpoint, body) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json();
}
async scanMessage(messageText, senderId) {
const threats = [];
// Extract and check phone numbers
const phoneRegex = /\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
const phones = messageText.match(phoneRegex) || [];
for (const phone of phones) {
const result = await this.callApi('/phone/lookup', {
phone_number: phone,
});
if (result.risk_score >= 40) {
threats.push({
type: 'risky_phone',
value: phone,
riskScore: result.risk_score,
reason: result.explanation,
lineType: result.signals.line_type,
});
}
}
// Extract and check URLs
const urlRegex = /https?:\/\/[^\s)]+/gi;
const urls = messageText.match(urlRegex) || [];
for (const url of urls) {
const result = await this.callApi('/url/lookup', { url });
if (result.risk_score >= 30) {
threats.push({
type: 'risky_url',
value: url,
riskScore: result.risk_score,
reason: result.explanation,
verdict: result.verdict,
});
}
}
return {
safe: threats.length === 0,
threats,
action: this.determineAction(threats, senderId),
};
}
determineAction(threats, senderId) {
if (threats.length === 0) return 'ALLOW';
const hasHighRisk = threats.some(t => t.riskScore >= 70);
const hasPhishing = threats.some(
t => t.type === 'risky_url' && t.verdict === 'phishing'
);
if (hasHighRisk || hasPhishing) {
return 'BLOCK_AND_FLAG_ACCOUNT';
}
const hasMediumRisk = threats.some(t => t.riskScore >= 40);
if (hasMediumRisk) {
return 'BLOCK_MESSAGE_WARN_RECIPIENT';
}
return 'WARN_RECIPIENT';
}
}
// Integration as message middleware
const scanner = new DatingAppSafetyScanner(process.env.SCAMVERIFY_API_KEY);
app.post('/api/messages/send', async (req, res) => {
const { senderId, recipientId, text } = req.body;
// Only scan messages that contain phone numbers or URLs
const hasContactInfo = /\d{3}[-.\s]?\d{3}[-.\s]?\d{4}|https?:\/\//i.test(text);
if (hasContactInfo) {
const scan = await scanner.scanMessage(text, senderId);
switch (scan.action) {
case 'BLOCK_AND_FLAG_ACCOUNT':
await flagAccount(senderId, {
reason: 'Sent message with high-risk contact details',
threats: scan.threats,
});
return res.status(403).json({
error: 'This message could not be sent.',
});
case 'BLOCK_MESSAGE_WARN_RECIPIENT':
await logBlockedMessage(senderId, recipientId, scan.threats);
return res.status(400).json({
error: 'This message contains content that may be unsafe.',
});
case 'WARN_RECIPIENT':
// Send the message but add a safety warning
await sendMessageWithWarning(senderId, recipientId, text, {
warning: 'This message contains external links or phone numbers. Be cautious.',
});
return res.json({ status: 'sent_with_warning' });
}
}
// No contact info detected, send normally
await sendMessage(senderId, recipientId, text);
return res.json({ status: 'sent' });
});import re
import requests
import os
class DatingAppSafetyScanner:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://scamverify.ai/api/v1"
def _call_api(self, endpoint: str, body: dict) -> dict:
response = requests.post(
f"{self.base_url}{endpoint}",
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
},
json=body,
)
response.raise_for_status()
return response.json()
def scan_message(self, message_text: str) -> dict:
threats = []
# Check phone numbers
phones = re.findall(r"\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}", message_text)
for phone in phones:
result = self._call_api("/phone/lookup", {"phone_number": phone})
if result["risk_score"] >= 40:
threats.append({
"type": "risky_phone",
"value": phone,
"risk_score": result["risk_score"],
"line_type": result["signals"]["line_type"],
})
# Check URLs
urls = re.findall(r"https?://[^\s)]+", message_text, re.IGNORECASE)
for url in urls:
result = self._call_api("/url/lookup", {"url": url})
if result["risk_score"] >= 30:
threats.append({
"type": "risky_url",
"value": url,
"risk_score": result["risk_score"],
"verdict": result["verdict"],
})
return {
"safe": len(threats) == 0,
"threats": threats,
}Threat Signal Matrix for Dating Platforms
| Scenario | Signals to Check | Risk Level | Action |
|---|---|---|---|
| New user registers with VoIP number | line_type: "voip" + high-risk carrier | Medium | Require photo verification before messaging |
| User shares phone number in DM | Phone risk_score >= 50 | High | Block message, warn recipient |
| User sends "verification" link | URL verdict = phishing | Critical | Block message, suspend sender |
| User sends shortened URL | URL risk_score >= 30 | Medium | Show safety interstitial to recipient |
| Reported profile's phone has FTC complaints | ftc_complaints > 0 | High | Suspend account pending investigation |
| User sends link to unknown domain | URL with no threat intel match | Low | Allow but display safety reminder |
Registration Screening
Block the most obvious fake accounts at signup before they waste your moderation team's time.
async function screenDatingSignup(phoneNumber) {
const result = await scanner.callApi('/phone/lookup', {
phone_number: phoneNumber,
});
// Romance scammers overwhelmingly use disposable VoIP
if (result.signals.line_type === 'voip' && result.risk_score >= 30) {
return {
approved: false,
reason: 'additional_verification_required',
// Require selfie or video verification instead of rejecting outright
nextStep: 'photo_verification',
};
}
if (result.risk_score >= 60) {
return {
approved: false,
reason: 'phone_not_accepted',
};
}
return { approved: true };
}Privacy-first scanning. The ScamVerify™ API checks phone numbers and URLs against threat intelligence databases. It does not read, store, or analyze the personal content of messages. You control what gets sent to the API and when.
Handling User Reports
When users report suspicious profiles, use the API to verify the reported details and build a case.
async function processUserReport(reportedUserId, reportDetails) {
const reportedUser = await getUser(reportedUserId);
const enrichment = {};
// Verify the reported user's registration phone
const phoneCheck = await scanner.callApi('/phone/lookup', {
phone_number: reportedUser.phone,
});
enrichment.phoneRisk = phoneCheck.risk_score;
enrichment.phoneVerdict = phoneCheck.verdict;
enrichment.lineType = phoneCheck.signals.line_type;
// If the report includes URLs the scammer shared
if (reportDetails.sharedUrls) {
enrichment.urlChecks = [];
for (const url of reportDetails.sharedUrls) {
const urlCheck = await scanner.callApi('/url/lookup', { url });
enrichment.urlChecks.push({
url,
riskScore: urlCheck.risk_score,
verdict: urlCheck.verdict,
});
}
}
// Auto-suspend if evidence is strong
const shouldAutoSuspend =
phoneCheck.risk_score >= 60 ||
enrichment.urlChecks?.some(u => u.riskScore >= 50);
if (shouldAutoSuspend) {
await suspendAccount(reportedUserId, {
reason: 'automated_scam_detection',
evidence: enrichment,
});
}
return enrichment;
}Best Practices
- Scan at the escalation point. The highest-value moment is when users share contact details in messages. That is where romance scams transition from social engineering to financial exploitation.
- Do not over-block. False positives destroy user trust on dating platforms. Use warnings and interstitials for medium-risk signals. Reserve hard blocks for confirmed phishing and high-risk numbers.
- Combine with behavioral signals. A new account that messages 50 people in 24 hours and shares the same URL with all of them is suspicious regardless of what ScamVerify™ returns. Use API signals alongside your own behavioral analytics.
- Respect user privacy. Only scan messages that contain phone numbers or URLs. Do not send entire conversation histories to any external API.
Getting Started
- Create an API key and set up your account
- Follow the Quickstart guide to make your first lookup
- Review the Phone Lookup API and URL Verification API reference docs
- Start with message scanning middleware, then expand to registration and report verification