--- title: "Mastodon — Triaging Crowdfunding / Mention-Spam Accounts" description: How to tell broadcast fundraising solicitation from genuine mentions, investigate the account and its origin instance with SQL + nodeinfo, and pick a proportionate moderation action. tags: - mastodon - moderation - abuse - federation - self-hosting created: 2026-06-22 updated: 2026-06-22 --- # Mastodon — Triaging Crowdfunding / Mention-Spam Accounts If you run a Mastodon instance, sooner or later you (or your users) start getting tagged by accounts you've never interacted with, posting donation appeals with a link and a wall of hashtags. Some are real people in desperate situations; some are recycled-link scams. Either way, when an account is **broadcasting a solicitation at you** rather than replying to you, it's a moderation question, not a conversation. This article is the runbook for telling the two apart, investigating both the **account** and its **origin instance**, and choosing an action that's proportionate instead of nuking eight years of legit federation over two bad actors. ## TL;DR - A mention is **broadcast spam**, not engagement, when it's a *standalone post* (not a reply) that *tags a large fixed list* of accounts and carries a *donation link*, usually from a *throwaway profile* on an *open-registration instance*. - Investigate before acting: pull the account's age/stats/bio and check whether the post is a reply or a 40-way blast (SQL below). Profile the origin instance via its public `nodeinfo`. - **Default action is an account-level block**, which also federates and removes their follow of you. Escalate to domain-limit / domain-block only when *one instance* produces *repeat offenders*. - Keep a log so single incidents that are actually a pattern become visible. ## Signals that a mention is broadcast solicitation Score it on how many of these hold: | Signal | Why it matters | |---|---| | **Standalone post, not a reply** (`in_reply_to_account_id IS NULL`) but still tags you | They're broadcasting, not responding | | **Tags a large fixed recipient list** (e.g. 40+) | Mass distribution; the same list reused across senders = coordination | | **Donation link** in post or bio (`chuffed.org`, `gofundme`, `paypal.me`, `ko-fi`) | The payload | | **Throwaway profile** — days old, few followers, follows you but you don't follow back | Disposable, baiting a profile view | | **Mass-follow ratio** — following thousands / few hundred followers | Engagement farming | | **"I am not a scammer" disclaimer** in bio | Known red-flag phrase | | **Origin instance: open registration, no approval** | Easy throwaway-account farm | > [!warning] Judgment, not a purity test > Many of these accounts are real people. The goal is not to adjudicate need — it's to stop *broadcast solicitation aimed at you* and track the *source instances*. Prefer the lightest action that stops it. ## Investigate the account Connect to the DB on the instance: ```bash ssh sudo -u postgres psql mastodon_production ``` **Profile + stats for a suspect** (age, post count, follower ratio, bio): ```sql SELECT a.username||'@'||a.domain, to_char(a.created_at,'YYYY-MM-DD') AS first_seen_locally, st.statuses_count, st.followers_count, st.following_count, left(regexp_replace(COALESCE(a.note,''),'<[^>]+>','','g'),200) AS bio FROM accounts a LEFT JOIN account_stats st ON st.account_id=a.id WHERE a.domain='' AND a.username=''; ``` **Is the mention a reply or a blast?** `standalone=t` with a high `num_tagged` is the tell: ```sql SELECT a.username, to_char(s.created_at,'YYYY-MM-DD HH24:MI') AS posted, s.in_reply_to_account_id IS NULL AS standalone, (SELECT count(*) FROM mentions mm WHERE mm.status_id=s.id) AS num_tagged FROM mentions m JOIN statuses s ON s.id=m.status_id JOIN accounts a ON a.id=s.account_id JOIN accounts me ON me.id=m.account_id AND me.username='' AND me.domain IS NULL WHERE a.username='' AND a.domain='' ORDER BY s.created_at DESC; ``` **All recent direct mentions of you** (sweep for the wider pattern): ```sql SELECT to_char(n.created_at,'YYYY-MM-DD HH24:MI') AS when, a.username||COALESCE('@'||a.domain,'@local') AS who, COALESCE(s.uri,'') AS uri, left(regexp_replace(COALESCE(s.text,''),'<[^>]+>','','g'),200) AS body FROM notifications n JOIN accounts recip ON recip.id=n.account_id AND recip.username='' AND recip.domain IS NULL JOIN accounts a ON a.id=n.from_account_id LEFT JOIN mentions m ON m.id=n.activity_id AND n.activity_type='Mention' LEFT JOIN statuses s ON s.id=m.status_id WHERE n.type='mention' ORDER BY n.created_at DESC LIMIT 40; ``` ## Profile the origin instance Don't judge an instance by one bad account. Pull its public metadata — no auth needed: ```bash # Software, version, user counts, registration policy NI=$(curl -s https:///.well-known/nodeinfo | python3 -c 'import sys,json;print(json.load(sys.stdin)["links"][-1]["href"])') curl -s "$NI" | python3 -m json.tool # software, openRegistrations, usage.users # Title, contact/admin, rules, registration approval flag curl -s https:///api/v2/instance | python3 -m json.tool ``` What to read off it: - **`openRegistrations: true` + `approval_required: false`** → throwaway-account farm; expect more of the same. - **`totalUsers` vs `activeMonth`** → a huge dormant base is typical of sign-up-and-leave farms. - **Federation age on your side** — how long you've known the instance, how many of its accounts you cache. A long, broad relationship argues *against* a domain block. - **The instance's own rules** — many ban "backlink accounts" / harassment, which the mass-tag fundraising violates. That makes **reporting to its admin a legitimate, in-policy path.** ```sql -- What your instance already knows about the domain SELECT (SELECT count(*) FROM accounts WHERE domain='') AS known_accounts, (SELECT count(*) FROM statuses s JOIN accounts a ON a.id=s.account_id WHERE a.domain='') AS cached_statuses, (SELECT to_char(min(created_at),'YYYY-MM-DD') FROM accounts WHERE domain='') AS first_seen, (SELECT count(*) FROM domain_blocks WHERE domain='') AS is_domain_blocked; ``` ## The escalation ladder | Level | Action | Effect | When | |---|---|---|---| | 1 | **Mute** | You stop seeing them; silent | Borderline; you don't want to cut them off | | 2 | **Block (account)** | Cuts mentions, removes their follow, federates to their instance | **Default first action** | | 3 | **Report** to source admin | Forwards the offending posts to their moderators | Repeat or egregious; in-policy on most instances | | 4 | **Domain-limit (silence)** | Their posts show only if you follow that account | One instance, multiple offenders | | 5 | **Domain-block (suspend)** | Severs all known accounts + federation | Instance is predominantly abuse | ### Blocking from a user account (federates + removes follow) There is no `tootctl accounts block`. Do it through the model's `BlockService` so it tears down the relationship and federates correctly: ```ruby # run as the mastodon user: # sudo -u mastodon bash -c 'cd /home/mastodon/live && RAILS_ENV=production bin/rails runner /tmp/block.rb' me = Account.find_by(username: "", domain: nil) %w[Handle1 Handle2].each do |u| t = Account.find_by(username: u, domain: "") next puts("NOTFOUND #{u}") if t.nil? BlockService.new.call(me, t) puts "BLOCKED #{u} blocking=#{me.blocking?(t)} they_follow_me=#{t.following?(me)}" end ``` `blocking=true` with `they_follow_me=false` confirms the block landed and the follow was severed. ### Instance-level actions Domain-limit / domain-block live in the admin UI (**Moderation → Federation**) or via `tootctl`: ```bash # Silence (limit) — posts hidden unless followed RAILS_ENV=production bin/tootctl domains ... # or set severity=silence in the admin UI # Suspend (block) the whole instance RAILS_ENV=production bin/tootctl ... # admin UI "Add domain block" is the safe path ``` > [!tip] Reach for the lightest hammer > A domain block is rarely the right first move against an established instance — you lose every legit account and years of federation to swat a couple of accounts. Block the accounts, report them to the source admin, and only escalate the *instance* when it demonstrates a sustained, multi-actor pattern. ## Keep a log Track offenders and source instances over time so a "one-off" that's actually a campaign becomes visible, and so domain-level decisions are evidence-based. A simple table — date, account, instance, signals, action — plus an instance-watch table with each source's registration policy and offender count is enough. ## Related - [Mastodon `--prune-profiles` Trap](mastodon-prune-profiles-trap.md) - [Mastodon DB Maintenance](mastodon-db-maintenance.md) - [Mastodon Federation](mastodon-federation.md)