Lookalike Audience Building Based on Site Data
Lookalike audiences—ad segments similar to your best customers. Built from conversion data on site and uploaded to ad systems (Meta, VK, Yandex) to find similar users.
Data Sources for Lookalike
Quality seed audiences yield better results:
| Segment | Size (min) | Quality |
|---|---|---|
| Buyers past 90 days | 500–1000 | Very high |
| Users with LTV > N | 500 | High |
| Trial → paid converters | 300 | High |
| Viewed key pages | 1000+ | Medium |
| All registered | 5000+ | Low |
Data Preparation and Hashing
Before uploading to ad systems, emails and phones must be hashed (SHA-256):
class AudienceExporter
{
public function exportHighValueCustomers(): array
{
return User::query()
->join('orders', 'orders.user_id', '=', 'users.id')
->where('orders.status', 'completed')
->where('orders.created_at', '>=', now()->subDays(90))
->groupBy('users.id', 'users.email', 'users.phone')
->havingRaw('SUM(orders.total) >= ?', [5000])
->get(['users.email', 'users.phone'])
->map(fn($user) => [
'email' => hash('sha256', strtolower(trim($user->email))),
'phone' => $user->phone ? hash('sha256', $this->normalizePhone($user->phone)) : null,
])
->toArray();
}
private function normalizePhone(string $phone): string
{
// Convert to E.164 format: +79991234567
$digits = preg_replace('/\D/', '', $phone);
if (strlen($digits) === 10) $digits = '7' . $digits;
return '+' . $digits;
}
public function exportToCsv(array $data, string $filename): string
{
$path = storage_path("app/audiences/{$filename}.csv");
$fp = fopen($path, 'w');
fputcsv($fp, ['email', 'phone']);
foreach ($data as $row) {
fputcsv($fp, [$row['email'], $row['phone'] ?? '']);
}
fclose($fp);
return $path;
}
}
Upload to Meta via API
class MetaAudienceService
{
private const API_VERSION = 'v19.0';
private string $accessToken;
private string $adAccountId;
public function createCustomAudience(string $name, string $description): string
{
$response = Http::post(
"https://graph.facebook.com/{$this->API_VERSION}/act_{$this->adAccountId}/customaudiences",
[
'name' => $name,
'subtype' => 'CUSTOM',
'description' => $description,
'customer_file_source' => 'USER_PROVIDED_ONLY',
'access_token' => $this->accessToken,
]
);
return $response->json('id');
}
public function uploadUsers(string $audienceId, array $hashedEmails): void
{
// Meta accepts batches of 10000 records
foreach (array_chunk($hashedEmails, 10000) as $chunk) {
$payload = [
'schema' => ['EMAIL_SHA256'],
'data' => array_map(fn($email) => [$email], $chunk),
];
Http::post(
"https://graph.facebook.com/{$this->API_VERSION}/{$audienceId}/users",
[
'payload' => json_encode($payload),
'access_token' => $this->accessToken,
]
);
}
}
public function createLookalike(string $sourceAudienceId, string $country, float $ratio = 0.01): string
{
// ratio: 0.01 = 1% (most similar), 0.10 = 10% (less similar, larger)
$response = Http::post(
"https://graph.facebook.com/{$this->API_VERSION}/act_{$this->adAccountId}/customaudiences",
[
'name' => "Lookalike {$country} {$ratio}",
'subtype' => 'LOOKALIKE',
'origin_audience_id' => $sourceAudienceId,
'lookalike_spec' => json_encode([
'type' => 'similarity',
'country' => $country,
'ratio' => $ratio,
]),
'access_token' => $this->accessToken,
]
);
return $response->json('id');
}
}
Upload to VK Ads
class VkAudienceService
{
public function uploadToRetargeting(string $name, array $hashedEmails): int
{
// VK accepts emails or phones openly (hashes itself)
// or SHA-256 with prefix
$content = implode("\n", $hashedEmails);
$response = Http::withToken($this->token)
->post('https://api.vk.com/method/ads.createTargetGroup', [
'account_id' => $this->accountId,
'name' => $name,
'v' => '5.199',
]);
$groupId = $response->json('response.id');
// Upload data
Http::withToken($this->token)
->post('https://api.vk.com/method/ads.importTargetContacts', [
'account_id' => $this->accountId,
'target_group_id' => $groupId,
'contacts' => $content,
'v' => '5.199',
]);
return $groupId;
}
}
Automatic Audience Updates
// Weekly update via Scheduler
class UpdateLookalikeAudiences implements ShouldQueue
{
public function handle(): void
{
$exporter = app(AudienceExporter::class);
$data = $exporter->exportHighValueCustomers();
$emails = array_column($data, 'email');
// Update in Meta
app(MetaAudienceService::class)->uploadUsers(
config('ads.meta.buyer_audience_id'),
$emails
);
Log::info("Lookalike audience updated", ['count' => count($emails)]);
}
}
Timeline
Audience export system with hashing and automatic Meta/VK updates: 4-6 business days.







