Runbook for telling broadcast fundraising solicitation from genuine mentions: signal checklist, SQL to investigate the account and its origin instance via nodeinfo, BlockService snippet, and a proportionate escalation ladder (mute -> block -> report -> domain-limit -> domain-block). Registered in SUMMARY.md and the self-hosting section index.
8.8 KiB
| title | description | tags | created | updated | |||||
|---|---|---|---|---|---|---|---|---|---|
| Mastodon — Triaging Crowdfunding / Mention-Spam Accounts | 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. |
|
2026-06-22 | 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:
ssh <your-mastodon-host>
sudo -u postgres psql mastodon_production
Profile + stats for a suspect (age, post count, follower ratio, bio):
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='<INSTANCE>' AND a.username='<HANDLE>';
Is the mention a reply or a blast? standalone=t with a high num_tagged is the tell:
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='<YOU>' AND me.domain IS NULL
WHERE a.username='<HANDLE>' AND a.domain='<INSTANCE>'
ORDER BY s.created_at DESC;
All recent direct mentions of you (sweep for the wider pattern):
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='<YOU>' 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:
# Software, version, user counts, registration policy
NI=$(curl -s https://<INSTANCE>/.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://<INSTANCE>/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.totalUsersvsactiveMonth→ 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.
-- What your instance already knows about the domain
SELECT (SELECT count(*) FROM accounts WHERE domain='<INSTANCE>') AS known_accounts,
(SELECT count(*) FROM statuses s JOIN accounts a ON a.id=s.account_id WHERE a.domain='<INSTANCE>') AS cached_statuses,
(SELECT to_char(min(created_at),'YYYY-MM-DD') FROM accounts WHERE domain='<INSTANCE>') AS first_seen,
(SELECT count(*) FROM domain_blocks WHERE domain='<INSTANCE>') 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:
# 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: "<YOU>", domain: nil)
%w[Handle1 Handle2].each do |u|
t = Account.find_by(username: u, domain: "<INSTANCE>")
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:
# 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.