Documentation
This guide covers three things:
- Public API setup — how to consume the read-only scammers endpoint.
- Discord moderation — block known scammers from your Discord server with ready-made bots in JavaScript and Python.
- Supabase Auth hook — block signups to your app when a Discord identity matches the scammer list.
API Overview
Your application exposes a read-only REST API that provides access to the scammers database. This endpoint is designed for integration with Discord bots, web applications, and other services that need to check against known scammers.
Endpoint Details
GET /api/scammers
Returns a paginated list of all scammers in the database with their associated images and sources.
Data Structure
The API returns a JSON object with the following structure:
{ "scammers": [ { "id": "uuid-v4-format", "name": "Scammer Name", "avatar_url": "https://example.com/avatar.jpg", "discord_id": "123456789012345678", "last_scam_location": "Berlin, Germany", "last_scam_service": "Creators Market", "scammer_images": [ { "image_url": "https://example.com/evidence1.jpg", "caption": "Screenshot of scam attempt" } ], "scammer_sources": [ { "source_name": "Discord Server Reports", "source_url": "https://discord.com/channels/..." } ] } ]}Field Descriptions
Unique identifier for each scammer record
The scammer's display name or username
Profile picture URL if available
Discord user ID for cross-platform identification
Geographic location of last reported scam
Platform where scam was reported (Discord, etc.)
Evidence images and screenshots
Original reports and references
Usage Examples
Basic Fetch Example
async function getScammers() { try { const response = await fetch('https://s0.renderdragon.org/api/scammers'); if (!response.ok) { throw new Error('HTTP error! status: ' + response.status); return []; } const data = await response.json(); return data.scammers; } catch (error) { console.error('Failed to fetch scammers:', error); return []; }}Discord Bot Integration
// Cache scammer IDs for quick lookupconst scammerIds = new Set();async function updateScammerCache() { const scammers = await getScammers(); scammerIds.clear(); scammers.forEach(scammer => { if (scammer.discord_id) { scammerIds.add(scammer.discord_id); } }); console.log('Loaded ' + scammerIds.size + ' scammer IDs');}// Check if user is a known scammerfunction isKnownScammer(userId) { return scammerIds.has(userId);}React Hook Example
import { useState, useEffect } from 'react';function useScammersData() { const [scammers, setScammers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchScammers() { try { setLoading(true); const response = await fetch('https://s0.renderdragon.org/api/scammers'); const data = await response.json(); setScammers(data.scammers); } catch (err) { setError(err.message); } finally { setLoading(false); } } fetchScammers(); }, []); return { scammers, loading, error };}Error Handling
Common HTTP Status Codes:
200- Success429- Rate limited (too many requests)500- Internal server error503- Service temporarily unavailable
Always implement proper error handling and consider implementing retry logic with exponential backoff.
Best Practices
- Cache the response: Store the API response locally and refresh periodically rather than calling the API on every user check.
- Handle rate limits: Implement proper backoff strategies if you exceed the rate limit.
- Validate Discord IDs: Discord user IDs are 17-19 digit numbers. Always validate the format.
- Consider privacy: Only store and process the data you actually need for your use case.
- Monitor for updates: Set up periodic refresh to catch new scammer reports.
JavaScript (discord.js)
Fetch the public scammers list and ban accounts on member join. Add a refresh command to re-pull the list.
// package.json// {// "type": "module",// "dependencies": { "discord.js": "^14.14.1", "node-fetch": "^3.3.2" }// }import { Client, GatewayIntentBits, Events } from "discord.js";import fetch from "node-fetch";// 1) Environmentconst DISCORD_TOKEN = process.env.DISCORD_TOKEN!; // Bot tokenconst API_BASE = process.env.API_BASE_URL || "https://s0.renderdragon.org"; // your app base URL// 2) Cache scammers from your public APIasync function fetchScammerIds(): Promise<Set<string>> { const res = await fetch(API_BASE + "/api/scammers"); if (!res.ok) throw new Error("Failed to fetch scammers"); const data = await res.json(); const ids = new Set<string>(); for (const s of data.scammers) { if (s.discord_id) ids.add(String(s.discord_id)); } return ids;}const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] });let banned = new Set<string>();client.once(Events.ClientReady, async () => { console.log("Logged in as " + (client.user?.tag ?? "")); banned = await fetchScammerIds(); console.log("Loaded " + banned.size + " scammer IDs");});// 3) Prevent scammers from joiningclient.on(Events.GuildMemberAdd, async (member) => { try { if (banned.has(member.id)) { await member.ban({ reason: "Listed as scammer" }); console.log("Banned " + member.user.tag); } } catch (e) { console.error("Failed banning member", e); }});// 4) Command to refresh cache (optional)client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; if (interaction.commandName === "refresh_scamlist") { banned = await fetchScammerIds(); await interaction.reply("Scam list refreshed. Entries: " + banned.size); }});client.login(DISCORD_TOKEN);Python (discord.py)
# requirements.txt# discord.py==2.4.0# aiohttp==3.9.5import osimport asyncioimport aiohttpimport discordfrom discord.ext import commandsTOKEN = os.getenv("DISCORD_TOKEN")API_BASE = os.getenv("API_BASE_URL", "https://s0.renderdragon.org")intents = discord.Intents.default()intents.members = Truebot = commands.Bot(command_prefix="!", intents=intents)banned: set[str] = set()async def get_scammer_ids() -> set[str]: async with aiohttp.ClientSession() as session: async with session.get(f"{API_BASE}/api/scammers") as resp: resp.raise_for_status() data = await resp.json() return {str(s.get("discord_id")) for s in data.get("scammers", []) if s.get("discord_id")}@bot.eventasync def on_ready(): global banned print(f"Logged in as {bot.user}") banned = await get_scammer_ids() print(f"Loaded {len(banned)} scammer IDs")@bot.eventasync def on_member_join(member: discord.Member): try: if member.id and str(member.id) in banned: await member.ban(reason="Listed as scammer") print(f"Banned {member}") except Exception as e: print("Ban failed", e)@bot.command()@commands.has_permissions(administrator=True)async def refresh_scamlist(ctx: commands.Context): global banned banned = await get_scammer_ids() await ctx.reply(f"Scam list refreshed. Entries: {len(banned)}")bot.run(TOKEN)Tip: run your bot with a process manager (PM2 / systemd) and store `DISCORD_TOKEN` and `API_BASE_URL` in environment variables.
Use the Before User Created Auth Hook to call an Edge Function that rejects signups when the incoming Discord provider_id is present in the public.scammers table. The function inspectsuser.identities first (recommended), then falls back to raw_user_meta_data.provider_id if present.
Edge Function
// supabase/functions/before-user-created/index.ts// Deploy as an HTTP Edge Function and configure as the Auth Hook: Before User Created// Supabase Dashboard -> Authentication -> Hooks -> before-user-created -> URL of this functionimport "jsr:@supabase/functions-js/edge-runtime.d.ts";import { createClient } from "https://esm.sh/@supabase/supabase-js@2";// Environment variables configured in the function settingsconst SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; // service role requiredconst supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { persistSession: false },});function extractDiscordId(payload: any): string | null { // Priority 1: identity provider record const identities = payload?.user?.identities ?? []; for (const id of identities) { if (id.identity_data?.provider === "discord" || id.provider === "discord") { const pid = id.identity_data?.provider_id || id.provider_id; if (pid) return String(pid); } } // Priority 2: raw_user_meta_data.provider_id const raw = payload?.user?.user_metadata || payload?.user?.raw_user_meta_data; if (raw?.provider_id) return String(raw.provider_id); return null;}Deno.serve(async (req: Request) => { try { const payload = await req.json(); const discordId = extractDiscordId(payload); if (!discordId) { // Allow signup if not a Discord signup return new Response("{}", { headers: { "Content-Type": "application/json" } }); } // Check if the discordId exists in our scammers table const { data, error } = await supabase .from("scammers") .select("id") .eq("discord_id", discordId) .limit(1); if (error) { console.error("DB error", error); // Fail-open or fail-closed? Choose fail-closed to be strict return new Response( JSON.stringify({ error: { http_code: 500, message: "Internal error" } }), { status: 200, headers: { "Content-Type": "application/json" }, } ); } if (data && data.length > 0) { // Block signup return new Response( JSON.stringify({ error: { http_code: 403, message: "Signup blocked due to scammer listing" } }), { status: 200, headers: { "Content-Type": "application/json" } } ); } // Allow signup return new Response("{}", { headers: { "Content-Type": "application/json" } }); } catch (e) { console.error(e); return new Response( JSON.stringify({ error: { http_code: 400, message: "Bad request" } }), { status: 200, headers: { "Content-Type": "application/json" }, } ); }});- Edge Function must use the
SERVICE_ROLEkey because it runs before the user exists. - Consider logging the hook payload with PII redaction for auditing.
- Keep the
scammerstable protected with RLS. The Edge Function bypasses RLS via service role.