Skip to content

Instantly share code, notes, and snippets.

@BlueBeret
Last active June 29, 2026 09:41
Show Gist options
  • Select an option

  • Save BlueBeret/1a692a84937e31f75efceb4e9cf610ba to your computer and use it in GitHub Desktop.

Select an option

Save BlueBeret/1a692a84937e31f75efceb4e9cf610ba to your computer and use it in GitHub Desktop.
Hide blue checkmark (verified) posts on X/Twitter - Tampermonkey userscript
// ==UserScript==
// @name Hide Blue Checkmark Posts (X / Twitter)
// @namespace https://gist.github.com/BlueBeret/1a692a84937e31f75efceb4e9cf610ba
// @version 1.1.0
// @description Hide posts from verified (blue checkmark) users on X/Twitter timelines, optionally keeping accounts you follow
// @author BlueBeret
// @match https://x.com/*
// @match https://twitter.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ----- settings -----
// ONLY_BLUE: true -> hide only the blue (Premium) checkmark, skip gold (business) and gray (government)
// ONLY_BLUE: false -> hide any verified badge
const ONLY_BLUE = true;
// COLLAPSE: true -> leave a small "hidden" placeholder, false -> fully remove the post from view
const COLLAPSE = true;
// EXCLUDE_FOLLOWING: true -> never hide posts from accounts you follow, even if blue-check.
// followed handles are harvested automatically from X's own API responses and cached in localStorage.
const EXCLUDE_FOLLOWING = true;
// optional manual keep-list, merged with the auto-detected following list (lowercase handles, no @)
const ALWAYS_KEEP = new Set([
// 'somefriend',
]);
// blue checkmark fill color used by X (brand blue). gold/gray badges use other colors.
const BLUE_COLORS = new Set(['rgb(29, 155, 240)', 'rgb(29,155,240)']);
// ---------------------------------------------------------------------------
// following list: harvested from X's GraphQL/API traffic + cached
// ---------------------------------------------------------------------------
const LS_KEY = 'bluecheck_following_v1';
const TTL = 7 * 24 * 60 * 60 * 1000; // refresh continuously from traffic; cache just bootstraps fast
const followed = new Set(ALWAYS_KEEP);
function loadFollowed() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return;
const { ts, handles } = JSON.parse(raw);
if (Date.now() - ts > TTL) return; // stale, will rebuild from live traffic
handles.forEach((h) => followed.add(h));
} catch (e) {}
}
function saveFollowed() {
try {
localStorage.setItem(
LS_KEY,
JSON.stringify({ ts: Date.now(), handles: [...followed] })
);
} catch (e) {}
}
// add a screen_name, return true if it was new
function addFollowed(sn) {
const h = String(sn).toLowerCase();
if (followed.has(h)) return false;
followed.add(h);
return true;
}
// walk any JSON blob looking for user objects flagged as followed.
// handles the various shapes X uses (following on the node, on legacy, or on relationship_perspectives)
function harvest(data) {
let added = false;
const stack = [data];
let steps = 0;
while (stack.length && steps < 50000) {
const node = stack.pop();
steps++;
if (!node || typeof node !== 'object') continue;
const sn =
node.screen_name ||
(node.core && node.core.screen_name) ||
(node.legacy && node.legacy.screen_name);
const fl =
node.following === true ||
(node.legacy && node.legacy.following === true) ||
(node.relationship_perspectives &&
node.relationship_perspectives.following === true);
if (sn && fl && addFollowed(sn)) added = true;
if (Array.isArray(node)) {
for (const v of node) if (v && typeof v === 'object') stack.push(v);
} else {
for (const k in node) {
const v = node[k];
if (v && typeof v === 'object') stack.push(v);
}
}
}
return added;
}
function harvestText(url, text) {
if (!text || !/x\.com|twitter\.com/.test(url)) return;
// cheap pre-filter before parsing
if (text.indexOf('screen_name') < 0 || text.indexOf('following') < 0) return;
let json;
try {
json = JSON.parse(text);
} catch (e) {
return;
}
if (harvest(json)) scheduleRescan();
}
// ----- intercept fetch -----
const origFetch = window.fetch;
if (origFetch) {
window.fetch = function (...args) {
return origFetch.apply(this, args).then((res) => {
try {
const url =
(res && res.url) ||
(typeof args[0] === 'string' ? args[0] : args[0] && args[0].url) ||
'';
if (/graphql|\/i\/api/.test(url)) {
res
.clone()
.text()
.then((t) => harvestText(url, t))
.catch(() => {});
}
} catch (e) {}
return res;
});
};
}
// ----- intercept XHR -----
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this.__bc_url = url;
return origOpen.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.send = function (...a) {
this.addEventListener('load', function () {
try {
const url = this.__bc_url || '';
if (!/graphql|\/i\/api/.test(url)) return;
if (this.responseType === 'json') {
if (this.response && harvest(this.response)) scheduleRescan();
} else if (this.responseType === '' || this.responseType === 'text') {
harvestText(url, this.responseText);
}
} catch (e) {}
});
return origSend.apply(this, a);
};
// pull the @handle for a tweet from its permalink (most reliable), with a text fallback
function getHandle(article) {
const timeLink =
(article.querySelector('a[href*="/status/"] time') || {}).parentElement ||
article.querySelector('a[href*="/status/"]');
const href = timeLink && timeLink.getAttribute('href');
const m = href && href.match(/^\/([^/]+)\/status\//);
if (m) return m[1].toLowerCase();
const nameBlock = article.querySelector('[data-testid="User-Name"]');
if (nameBlock) {
for (const s of nameBlock.querySelectorAll('span')) {
if (/^@\w{1,15}$/.test(s.textContent)) return s.textContent.slice(1).toLowerCase();
}
}
return null;
}
function isFollowed(article) {
if (!EXCLUDE_FOLLOWING) return false;
const h = getHandle(article);
return !!h && followed.has(h);
}
// ---------------------------------------------------------------------------
// badge detection (unchanged)
// ---------------------------------------------------------------------------
// returns true if the badge inside the author area is a blue checkmark
function isBlueBadge(badge) {
if (!ONLY_BLUE) return true;
// gold business badges contain a gradient; blue is a solid fill
if (badge.querySelector('linearGradient')) return false;
const colored = badge.querySelector('[fill]') || badge;
const fill =
colored.getAttribute('fill') ||
getComputedStyle(colored).fill ||
getComputedStyle(badge).color;
return BLUE_COLORS.has(fill) || fill === '#1d9bf0';
}
// a post is the tweet article; the badge must sit in the author name block
function hasBlueCheck(article) {
const nameBlock = article.querySelector('[data-testid="User-Name"]') || article;
const badge = nameBlock.querySelector('svg[data-testid="icon-verified"]');
if (!badge) return false;
return isBlueBadge(badge);
}
// ---------------------------------------------------------------------------
// hide / unhide
// ---------------------------------------------------------------------------
const hiddenState = new WeakMap(); // article -> { el, note }
function hidePost(article) {
if (article.dataset.bluecheckHidden) return;
article.dataset.bluecheckHidden = '1';
const cell = article.closest('[data-testid="cellInnerDiv"]');
if (COLLAPSE) {
// collapse the cell but keep a tiny note so the timeline doesn't jump
article.style.display = 'none';
const note = document.createElement('div');
note.className = 'bluecheck-note';
note.textContent = 'Post hidden (blue checkmark)';
note.style.cssText =
'padding:8px 16px;font-size:13px;color:#71767b;border-bottom:1px solid #2f3336;';
(cell || article).appendChild(note);
hiddenState.set(article, { el: article, note });
} else {
const el = cell || article;
el.style.display = 'none';
hiddenState.set(article, { el, note: null });
}
}
// reverse a hide when we later learn the author is followed (or it no longer qualifies)
function unhidePost(article) {
if (!article.dataset.bluecheckHidden) return;
delete article.dataset.bluecheckHidden;
const st = hiddenState.get(article);
if (st) {
if (st.el) st.el.style.display = '';
if (st.note) st.note.remove();
hiddenState.delete(article);
} else {
article.style.display = '';
}
}
function processArticle(a) {
const blue = hasBlueCheck(a);
const keep = blue && isFollowed(a);
if (blue && !keep) hidePost(a);
else if (a.dataset.bluecheckHidden) unhidePost(a);
}
function scan(root) {
const articles = root.querySelectorAll
? root.querySelectorAll('article[data-testid="tweet"]')
: [];
articles.forEach(processArticle);
// also handle the root itself if it's an article
if (root.matches && root.matches('article[data-testid="tweet"]')) {
processArticle(root);
}
}
// debounced rescan, fired whenever new follow data is learned
let rescanQueued = false;
function scheduleRescan() {
if (rescanQueued) return;
rescanQueued = true;
setTimeout(() => {
rescanQueued = false;
saveFollowed();
scan(document);
}, 200);
}
// ---------------------------------------------------------------------------
// boot
// ---------------------------------------------------------------------------
loadFollowed();
function start() {
scan(document);
// the timeline streams in new posts, so watch for them
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType === 1) scan(node);
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// @run-at document-start lets us hook fetch/XHR before X loads; wait for body to scan
if (document.body) start();
else document.addEventListener('DOMContentLoaded', start, { once: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment