Tworzysz nowy SaaS i widzisz pierwsze konta, które wyglądają na fake - albo masz już produkcję, a spammerzy zaczęli puchnąć. Poniżej kompletny przewodnik: jakie sygnały zbierać, jak liczyć trust score, jakie techniki obronne mają sens, oraz jak to wszystko utrzymać w produkcji bez zalania moderacji ticketami. Konkretni providerzy są w osobnym wpisie Anti-fraud i CAPTCHA dla SaaSów.
W praktyce nie chodzi o jedno pole is_spam, tylko o zestaw sygnałów, z których budujesz score. Trzymaj je przy accounts lub w osobnej tabeli account_signals żeby główna nie puchła.
Sygnały rejestracji (zbierane raz, przy signupie)
| Pole | Skąd | Po co |
|---|---|---|
| signup_ip, signup_country, signup_asn | request | ASN łapie sieci hostingowe (DigitalOcean, Hetzner) używane przez boty |
| signup_user_agent | request | Stare lub nietypowe UA = bot |
| signup_device_fingerprint | FingerprintJS / ThumbmarkJS | Jedna farma kont = ten sam fingerprint |
| signup_is_vpn / proxy / tor / datacenter | IPQualityScore, MaxMind, AbuseIPDB | VPN + disposable email = 90% spam |
| email_domain, email_is_disposable, email_is_freemail, email_mx_valid | własna logika + lista ivolo | Mailinator/tempmail to czerwona flaga |
| email_verified_at | klik w link | Najtańszy filtr |
| captcha_score | Turnstile / reCAPTCHA v3 | 0.0-1.0, niski score = automat |
| referrer, utm_source | request | Boty mają puste albo identyczne UTM-y |
| signup_fill_time_ms | timer JS | Bot wypełni 200ms, człowiek 15s |
| honeypot_triggered | ukryte pole | Tylko bot wypełni pole z display: none
|
Sygnały tożsamości (wzmacniają zaufanie)
- weryfikacja telefonu - data, kraj, typ carriera (mobile vs VOIP). Numery z Twilio i Google Voice są podejrzane
- weryfikacja płatności - data, kraj billingu, ostatnie 4 cyfry karty
-
billing_country_mismatch- bool, kraj karty inny niż signup/IP
Sygnały behawioralne (aktualizowane na bieżąco)
-
last_login_at,login_count,failed_login_count -
unique_ips_count_30d- konto z 8 krajów w tygodniu zwykle skradzione albo współdzielone -
unique_devices_count,unique_countries_count_30d -
actions_per_hour_max- bot wali stałym tempem, człowiek ma piki -
api_calls_count,api_error_rate- wysoki error rate to skanowanie albo scraping
Sygnały reputacji w produkcie
-
content_flagged_count- ile razy treści użytkownika oflagowano -
reports_received_count- zgłoszenia od innych userów -
outbound_emails_bounce_rate- spammer zbierający adresy ma 60% bounce -
invites_sent_countiinvites_accepted_ratio- asymetria zdradza spam
Pola decyzyjne (tych używa aplikacja)
| Pole | Typ | Do czego |
|---|---|---|
status |
enum: pending_verification, active, restricted, shadow_banned, suspended, banned
|
Centralne pole sterujące |
trust_score |
int 0-100 | Ważona suma sygnałów, liczona w jobie cyklicznie |
risk_flags |
JSONB albo bitmask |
disposable_email, vpn, velocity_abuse itp. |
rate_limit_tier |
enum/int | Niskozaufane konta dostają niższe limity automatycznie |
manual_review_status, reviewed_by, reviewed_at
|
Audit ręcznych decyzji | |
suspended_at, suspended_reason
|
Historia banów |
Techniki, których same kolumny nie załatwią
Progressive trust - nowe konto może mało: limit zaproszeń, brak publicznych linków, brak webhooków na zewnątrz. Zaufanie rośnie z czasem i z weryfikacjami.
Shadow ban - spammer widzi swoje treści jako opublikowane, ale inni userzy ich nie widzą. Nie próbuje obchodzić bana, bo nie wie, że jest banem. Implementacja w Rails: default_scope { where(shadow_banned: false) } na publicznych scope’ach, ale bez tego w widoku samego autora.
Velocity rules - reguła “więcej niż X kont z tego ASN w 1h” albo “więcej niż X zaproszeń z konta w 10 min” przełącza automatycznie na restricted. Najprostsza implementacja w Redisie z TTL.
Honeypoty na formularzach - ukryte pole website albo phone2, które tylko bot wypełni. display: none + aria-hidden="true" + tabindex=-1.
Device/browser fingerprinting - to samo fingerprint na 50 kontach = farma. FingerprintJS w wersji OSS daje 70% wartości za darmo.
Email reputation - sprawdzaj wiek domeny, rekord SPF/DMARC, czy domena nie jest tylko parkowana. Domena z whois <30 dni + brak DMARC = czerwona flaga.
Łańcuch weryfikacji - email -> telefon -> karta. Każdy krok odsiewa kolejną warstwę spamu.
Asynchroniczny scoring - po signupie wrzucasz job do kolejki, który dopytuje IPQualityScore/Sift/Castle i aktualizuje trust_score. Nie blokujesz UX rejestracji.
Analiza grafowa - najmocniejsza broń przeciw farmom
Pojedyncze konto może wyglądać czysto, ale farma 200 kont zdradzi się w relacjach. Buduj graf i licz cechy klastra:
- wspólny fingerprint / IP / ASN / payment_fingerprint to krawędź między kontami
- connected components - jeśli komponent ma >N kont założonych w <M dni, automatyczny review całego klastra
- invite chain - kto kogo zaprosił. Spammerzy często masowo zapraszają się nawzajem żeby podbić sztuczny “trust”
- wspólne treści - hash/simhash treści, te same linki w opisach, ten sam numer telefonu w profilach
W praktyce nocny job przelatuje grafem, oznacza klastry, a trust_score konta dziedziczy karę z klastra. Próg ważny - pojedyncze połączenie nic nie znaczy, 5 wspólnych sygnałów już tak.
Feedback loop - bez tego system gnije
Każda ręczna decyzja moderatora (ban / unban / “to był false positive”) MUSI wracać do systemu jako label:
# tabela account_decisions
account_id, decision, reason, decided_by, decided_at, signals_snapshot (JSONB)
Snapshot sygnałów w momencie decyzji - żeby móc potem trenować model na realnych danych. Raz na tydzień regresja logistyczna na tych labelach generuje nowe wagi do trust_score. Bez tego wagi są na czuja i z czasem rozjeżdżają się z rzeczywistością.
Na start nie potrzebujesz ML, wystarczy ważona suma:
disposable_email: -30
vpn_signup: -15
phone_verified: +20
payment_verified: +30
account_age_30d: +10
ML dokładasz dopiero gdy masz >1000 oznaczonych decyzji.
Appeal / odwołania
To, czego nikt nie robi, a powinien.
- zbanowany user dostaje formularz odwołania, nawet jeśli to bot. Po pierwsze koszt obsługi mały, po drugie masz dane do feedback loop
- każde uznane odwołanie to silny sygnał false-positive
- bez tego średnio raz na kwartał banujesz cudzoziemca z VPN-em z kawiarni i masz publiczny dramat na X
Metryki, które musisz widzieć codziennie
| Metryka | Po co |
|---|---|
| Signup funnel by trust tier | Ilu w pending, active, restricted per dzień |
| Time-to-detect | Minuty od signupu do banu - im krócej tym lepiej |
| False positive rate | % unbanowań po odwołaniu |
| Spam reach | Ile treści od konta dotarło do innych zanim trafiło na shadow ban |
| Top 20 ASN signupów | Nagły skok z jednego ASN = atak w toku |
Bez dashboardu nie wiesz, czy system działa, czy tylko miele.
RODO / legal (PL i UE)
Trzymanie IP, fingerprintu i geo to dane osobowe.
- podstawa prawna: prawnie uzasadniony interes (Art. 6.1.f). Security to klasyk, ale udokumentuj ocenę interesów
- retencja: trust signals max 12-24 mies. od ostatniej aktywności. Zrób cron czyszczący
- polityka prywatności musi wymieniać IPQualityScore/Sift jako sub-procesora
- prawo dostępu - user może poprosić o swoje sygnały, przygotuj export
- profilowanie automatyczne (Art. 22) - automatyczny hard ban bez człowieka jest formalnie problematyczny. Bezpieczniej:
restrictedautomatycznie + ban dopiero po review przez moderatora
Edge case’y, które zawsze wracają
- korporacyjne IP - 500 userów z jednego NAT-u wygląda jak farma. Whitelistuj znane firmowe ASN-y
- mobile carrier-grade NAT - operatorzy komórkowi dzielą jedno IP między tysiące userów
- iCloud Private Relay i Apple Hide My Email - wygląda jak VPN + disposable email, a to legit Apple
- studenci i akademiki - jeden budynek = jeden IP, dużo kont
- powracający user po latach - stare konto + nowe urządzenie + VPN z urlopu = fałszywy alarm
Dla każdego z tych przypadków potrzebujesz allow-listy albo reguły “human review zamiast auto-ban”.
Wzorce czysto Rails-owe
-
Scoring jako job:
TrustScoreRecalculationJob.perform_later(account_id)triggerowany zafter_create_commitna Account oraz cyklicznie z crona raz dziennie dla aktywnych - kolumna
trust_scorecache’owana waccounts, ale liczona zaccount_signals. Nie licz on-the-fly w każdym żądaniu - concern
Trustablena modelach robiących ryzykowne akcje (wysyłka emaila, publikacja publiczna, zaproszenia) sprawdzaaccount.trust_score >= thresholdzanim odpali -
Rate limiter per tier: Rack::Attack albo własny w Redisie. Klucz
"rate:#{account_id}:#{action}", limit czytany zaccount.rate_limit_tier - shadow ban przez scope -
default_scopena publicznych widokach Postów/Komentarzy, ale bez tego w widoku samego autora (autor widzi swoje treści, reszta nie)
Co najczęściej idzie źle
- Za agresywny start - banujesz 5% legit userów, support tonie w ticketach, biznes każe wyłączyć system. Zaczynaj od shadow ban i monitoringu, hard bany dopiero po tygodniach kalibracji
- Brak shadow ban - spammer widzi 403, zmienia IP/email, wraca w 5 min. Shadow ban kupuje ci dni
- Hardcoded thresholds bez A/B - nie wiesz, czy
trust_score < 30 = restrictedjest lepsze niż< 40 - Sygnały bez TTL - konto z 2019 ma flagę
vpn_signupi nadal jest karane, choć od 5 lat loguje się normalnie. Każdy sygnał musi mieć datę i wagę malejącą z czasem - Brak audit logu decyzji - moderator banuje, nie wiadomo dlaczego, user się odwołuje, nikt nie potrafi odtworzyć
MVP - kolejność wdrożenia
- Cloudflare Turnstile + email verification + lista disposable - odsiewa 70% śmieci za darmo
- Tabela
account_signals(JSONB) +trust_score(int) +statusenum waccounts - Async job po signupie: IPQualityScore lookup -> zapis sygnałów -> przeliczenie score
- Shadow ban dla
trust_score < 20zamiast hard ban - Prosty dashboard moderacyjny + appeal form
- Feedback loop - zapis decyzji moderatorów ze snapshotem sygnałów
- Po 1-2 mies. danych dopiero wtedy graf klastrów i ewentualny ML
Reszta (carrier check, device fingerprint farm detection, behavioral biometrics) wchodzi dopiero gdy zobaczysz konkretny wektor ataku, którego MVP nie łapie. Najtańsze 80% wartości daje pierwsze 4 punkty z listy.