Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Detect and analyze abusive accounts on Pollinations. IP clustering, multi-signal scoring, ban recommendations. Use when investigating abuse, bot farms, or suspicious usage patterns.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 353% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 174% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 78% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 322% | 0% |
tb): Must be authenticatedenter.pollinations.ai/observability/ (has .tinyb config)Tinybird query pattern:
bashcd enter.pollinations.ai/observability tb --cloud sql "SELECT ... FROM generation_event_v2 ..."
> Workspace: This skill is prod-only. The .tinyb in observability/ points to the pollinations_enter workspace (prod traffic). Staging traffic lives in pollinations_enter_staging and has no real abuse signal — don't waste time analyzing it. To pin a query to staging anyway (e.g. testing a new scoring query), set TB_TOKEN=<staging_admin_token> for that one command.
> Quoting: Use double quotes for the SQL string. Use single quotes inside SQL. Avoid != with $'...' shell quoting (escaping issues) — prefer NOT IN ('undefined', '') instead.
> tb CLI caps at 100 rows. For large result sets, use the HTTP API: > bash > TB_TOKEN=$(python3 -c "import json; print(json.load(open('.tinyb'))['token'])") > curl -s "https://api.europe-west2.gcp.tinybird.co/v0/sql" \ > -H "Authorization: Bearer $TB_TOKEN" \ > --data-urlencode "q=SELECT ... FORMAT JSONCompact" | python3 -c "import json,sys; ..." >
Six signals, each weighted independently:
| Signal | Max Points | Threshold | What it catches | |--------|-----------|-----------|-----------------| | IP cluster size | 30 | cluster * 0.15 | Multiple users sharing same IP hash | | Zero pack spend | 15 | spend = 0 | No paid pack usage | | Error rate | 15 | >= 95% (15pts), >= 70% (10pts) | Bots hammering failing endpoints | | Moderation flags | 15 | >= 90% sexual (15pts), >= 50% (8pts) | NSFW generation bots | | Disposable email | 15 | hotmail/outlook/proton + no spend | Random-string throwaway emails | | IP rotation | 10 | >= 50 IPs (10pts), >= 20 (5pts) | Rotating through many exit IPs |
Score interpretation:
| Score | Action | False positive risk | |-------|--------|-------------------| | 90-100 | Ban immediately | Very low | | 70-89 | Ban after quick review | Low | | 40-69 | Manual review needed | Medium | | 10-39 | Monitor only | High — many legit users with NSFW or errors | | 0-9 | Clean | N/A |
For accounts generating massive failing traffic with no paid pack usage, a simpler signal is sufficient:
zero pack spend + 95%+ error rate + 1000+ requests/weekThis catches bot farm accounts that are already rate-limited (no spendable balance) but still hammering the API with failing requests. These accounts waste server resources with zero legitimate usage.
Query:
sqlSELECT user_id FROM ( SELECT g.user_id, count() as total_reqs, round(sumIf(g.total_price, g.selected_meter_slug IN ('v1:meter:pack', 'local:pack')), 4) as pack_spend, countIf(g.response_status >= 400) * 100.0 / count() as err_pct FROM generation_event_v2 g WHERE g.start_time >= now() - INTERVAL 7 DAY AND g.is_final AND g.user_id NOT IN ('undefined', '') GROUP BY g.user_id HAVING total_reqs >= 1000 ) WHERE pack_spend = 0 AND err_pct >= 95
> Note: tb --cloud sql caps output at 100 rows. For large result sets, use the Tinybird HTTP API with FORMAT JSONCompact.
Returns all users with abuse score, sorted by score descending. The spend signal uses selected_meter_slug to distinguish paid pack consumption from the other active balance bucket.
sqlSELECT user_id, github_username, email, total_reqs, pack_spend, total_spend, max_ip_cluster, distinct_ips, round(err_pct, 1) as err_pct, round(sex_pct, 1) as sex_pct, abuse_score FROM ( SELECT g.user_id, u.github_username, u.email, count() as total_reqs, round(sumIf(g.total_price, g.selected_meter_slug IN ('v1:meter:pack', 'local:pack')), 4) as pack_spend, round(sum(g.total_price), 4) as total_spend, max(coalesce(ips.ip_cluster_size, 0)) as max_ip_cluster, countDistinct(g.ip_hash) as distinct_ips, countIf(g.response_status >= 400) * 100.0 / count() as err_pct, countIf(g.moderation_prompt_sexual_severity NOT IN ('safe', '')) * 100.0 / count() as sex_pct, round( least(30, max(coalesce(ips.ip_cluster_size, 0)) * 0.15) + multiIf( splitByChar('@', u.email)[2] = 'proton.me' AND pack_spend = 0, 15, splitByChar('@', u.email)[2] = 'hotmail.com' AND pack_spend = 0, 12, splitByChar('@', u.email)[2] = 'outlook.com' AND pack_spend = 0, 10, 0) + if(pack_spend = 0, 15, 0) + if(countIf(g.response_status >= 400) * 100.0 / count() >= 95, 15, if(countIf(g.response_status >= 400) * 100.0 / count() >= 70, 10, 0)) + if(countIf(g.moderation_prompt_sexual_severity NOT IN ('safe', '')) * 100.0 / count() >= 90, 15, if(countIf(g.moderation_prompt_sexual_severity NOT IN ('safe', '')) * 100.0 / count() >= 50, 8, 0)) + if(countDistinct(g.ip_hash) >= 50, 10, if(countDistinct(g.ip_hash) >= 20, 5, 0)) , 0) as abuse_score FROM generation_event_v2 g LEFT JOIN d1_user u ON g.user_id = u.id AND u.synced_at = (SELECT max(synced_at) FROM d1_user) LEFT JOIN ( SELECT ip_hash, count(DISTINCT user_id) as ip_cluster_size FROM generation_event_v2 WHERE start_time >= now() - INTERVAL 7 DAY AND is_final AND ip_hash NOT IN ('undefined', '') AND user_id NOT IN ('undefined', '') GROUP BY ip_hash ) ips ON g.ip_hash = ips.ip_hash WHERE g.start_time >= now() - INTERVAL 7 DAY AND g.is_final AND g.user_id NOT IN ('undefined', '') GROUP BY g.user_id, u.github_username, u.email HAVING total_reqs >= 5 ) WHERE abuse_score >= 40 ORDER BY abuse_score DESC, total_reqs DESC LIMIT 100
Find IPs shared by many users (bot farm detection):
sqlSELECT ip_subnet, ip_hash, count(DISTINCT user_id) as unique_users, count() as total_requests, dateDiff('minute', min(start_time), max(start_time)) as span_min FROM generation_event_v2 WHERE start_time >= now() - INTERVAL 7 DAY AND is_final AND ip_hash NOT IN ('undefined', '') AND user_id NOT IN ('undefined', '') GROUP BY ip_hash, ip_subnet HAVING unique_users >= 10 ORDER BY unique_users DESC LIMIT 30
sqlSELECT DISTINCT g.user_id, u.github_username, u.email, sumIf(g.total_price, g.selected_meter_slug IN ('v1:meter:pack', 'local:pack')) as pack_spend, sum(g.total_price) as total_spend FROM generation_event_v2 g LEFT JOIN d1_user u ON g.user_id = u.id AND u.synced_at = (SELECT max(synced_at) FROM d1_user) WHERE g.start_time >= now() - INTERVAL 7 DAY AND g.is_final AND g.ip_hash = '<IP_HASH_HERE>' AND g.user_id NOT IN ('undefined', '') GROUP BY g.user_id, u.github_username, u.email ORDER BY pack_spend DESC
sqlSELECT multiIf(abuse_score >= 90, '90-100 definite', abuse_score >= 70, '70-89 likely', abuse_score >= 40, '40-69 suspicious', abuse_score >= 10, '10-39 low_risk', '0-9 clean') as bucket, count() as users, round(sum(total_spend), 2) as spend, sum(total_reqs) as requests FROM ( /* ... full scoring subquery from #1 ... */ ) GROUP BY bucket ORDER BY bucket DESC
sqlSELECT user_id FROM ( /* ... full scoring subquery from #1 ... */ ) WHERE abuse_score >= 90
Always check before banning:
| Pattern | Why it's a false positive | How to detect | |---------|--------------------------|---------------| | Cloudflare WARP/Workers | IPv6 2a06:98c0:3600:: — legit users behind Cloudflare | Check ip_subnet starts with 2a06:98c0 | | VPN/proxy clusters | Multiple real users behind same VPN exit | Check if cluster has paying users with real emails | | Chinese CGNAT | Mobile carriers (China Mobile/Unicom/Telecom) share IPs via NAT | Cross-reference with email pattern + spend | | Free balance usage | Accounts show small "spend" from non-pack balance, not real payment | Check pack_spend — only pack spend is real payment | | High NSFW, legit user | Some paying users generate NSFW content legitimately | Check pack spend > $5 — real customers |
Safe to ban (high confidence):
reksely/notreksely/rekselicha)Needs review:
2a06:98c0:*)The ban system uses Better Auth fields on the user table in Cloudflare D1:
| Field | Type | Description | |-------|------|-------------| | banned | boolean (integer 0/1) | Set to 1 to ban | | ban_reason | text | Shown in 403 error response | | ban_expires | integer (epoch ms) | NULL for permanent, epoch ms for temporary |
Enforcement (src/middleware/auth.ts):
assertNotBanned() runs on every authenticated request (session + API key)banned = 1 and not expired → HTTP 403 with ban reasonban_expires is set and has passed → ban is automatically liftedThere is no admin API for banning — use wrangler d1 execute directly.
bash# Single user ban (from enter.pollinations.ai/ directory) npx wrangler d1 execute production-pollinations-enter-db --remote \ --command "UPDATE user SET banned = 1, ban_reason = 'Bot farm abuse' WHERE id = '<USER_ID>'" # Batch ban (from a file of user IDs, one per line) IDS=$(cat user_ids_to_ban.txt | sed "s/^/'/;s/$/'/" | paste -sd, -) npx wrangler d1 execute production-pollinations-enter-db --remote \ --command "UPDATE user SET banned = 1, ban_reason = 'Automated: bot farm abuse' WHERE id IN ($IDS)" # Unban a user (if false positive) npx wrangler d1 execute production-pollinations-enter-db --remote \ --command "UPDATE user SET banned = 0, ban_reason = NULL WHERE id = '<USER_ID>'" # Temporary ban (expires after 7 days) npx wrangler d1 execute production-pollinations-enter-db --remote \ --command "UPDATE user SET banned = 1, ban_reason = 'Temporary: rate abuse', ban_expires = $(date -v+7d +%s)000 WHERE id = '<USER_ID>'"
D1 database names:
production-pollinations-enter-dbstaging-pollinations-enter-dbdevelopment-pollinations-enter-dbCharacteristics discovered:
lhanbqkf6005@hotmail.com)bomteupted-bsfo, jwolfwersenmroom)| Table | Key columns for abuse | |-------|----------------------| | generation_event_v2 | user_id, ip_hash, ip_subnet, response_status, total_price, selected_meter_slug, moderation_prompt_*, event_type | | d1_user | id, email, github_username, banned, banReason, created_at |
IP implementation (src/middleware/track.ts):
ip_hash: Salted SHA-256 of full IP (irreversible)ip_subnet: Truncated to /24 (IPv4) or /48 (IPv6)cf-connecting-ip header| Date | Action | Count | Details | |------|--------|-------|---------| | 2026-03-06 | Banned bot farm | 277 | IP cluster ≥100, 95%+ errors, $0 pack spend | | 2026-03-06 | Rate-limited bot farm | 42 | Same bot farm, no pack spend | | 2026-03-06 | Rate-limited bot farm | 59 | Multi-signal: IP clusters, gibberish suffixes, disposable emails, hammering |
d1_user table in Tinybird syncs periodically (not real-time). After banning on D1, Tinybird data is stale — verify actions on D1 directly.pack_spend to catch accounts with no real payment.-boop, -a11y, -max, -sudo, -cmd, -stack, -pixel, -dot, -beep, -commits, -ops, -dotcom, -lang, -bit. These are auto-generated.Other measured skills in the registry, with their headline benchmark lift.