Replace Twitter Blue verified check marks with rotating blue poo emojis

Replace Twitter Blue verified check marks with rotating blue poo emojis


5 min read

With all the news lately around Twitter and the new $8 Twitter Blue subscription, I thought it would be fun to be able to identify which Twitter users are Twitter Blue subscribers. And what better way than to replace their checkmark with a blue poo emoji? I call it Tweeter Bloo.


The Solution: A Chrome extension

To solve this very serious and important problem, I decided to build a Chrome extension.

Feature 1: Profile page

The first feature I built was displaying the poo emoji on the profile page. Once the profile page was loaded, I found a checkmark. Then, if the checkmark exists, I found out what kind of checkmark it was. I did this by programmatically clicking the checkmark to open the hover card element, scanning the text for Twitter Blue, then closing it again.

Then, once I've been able to determine if a user's checkmark was from Twitter blue, I replaced it with a blue poo emoji.

This is the query selector I used to help with this. I opted for this one over the aria label because it is localization-friendly as a test ID:

[data-testid="UserName"] svg

Feature 2: Home feed

I knew before starting that injecting poos on the home feed would not be an easy task. There was nothing in the DOM (that I could find) that could help me with this task. So I needed to get creative with it.

The normal flow on the home feed is that if you wanted to find out how they are verified, you'd click their username or icon, and then be taken to their profile page. From there, you would click the icon and read the text in the card that pops up. That flow wasn't going to work for me, or at least I couldn't immediately think of how to implement that without navigating away from the page. If you have any ideas, let me know!

Network Requests

So, I decided to look into the network requests. I was able to find a GraphQL request that included the first 26 items in the home feed. I parsed this large response and looked for the property is_blue_verified as well as the username and stored a hash map of usernames to blue verified status.

Finding this network request initially took quite a long time. Twitter is a big app, and the responses were nested pretty deeplyโ€”the nature of GraphQL. Here's an example of a selector (that often crashed) that I used:


๐Ÿ’ฉ Pagination

Pagination to support the load-more functionality didn't go so well, and unfortunately this solution only worked for the first page of results, which contained around 25 items.

While I did see some more requests at all.json with cursors that included the upper and lower bounds for paginating, I wasn't able to see those requests being made and therefore wasn't able to continue to add poos. If you know how the feed's load-more functionality works, feel free to share!

Attaching the debuggerrr

In order for this to work, I needed to use the Chrome extension's debugger API to listen to network requests. I had not worked with the debugger API before.

It is quite a hack and unfortunately displays a debugging message to the user, so it's pretty obvious it's running.

The web request API does not expose response bodies, only status codes and headers, so I couldn't use that simpler API.


Content scripts and Background scripts

If you aren't familiar with Chrome extension development, the content scripts are the ones that run on the page and have access to the DOM.

The content scripts are where any DOM changes happen, including styles and injecting elements.

While all of the DOM manipulation happened in the content scripts, a couple of other things happened in the background script:

  • detecting page navigation
  • listening to networking request responses

We needed to debug the networking responses in order to capture the timeline and verified status information.

Since the app is a single page app (built in React), there are no page refreshes when you click around, so the Chrome extension wouldn't automatically reload. For that reason, I needed to listen to URL changes.

The background scripts cannot manipulate the DOM, and the content scripts cannot listen to network requests or detect navigation. This means that in order to communicate between the two, I needed to do message passing over Chrome's tabs.sendMessage() API.

Feature 3: Animations

And finally, to tie it all together, I added an animation that makes the blue poo rotate indefinitely. It was definitely a silly feature to add, but is low effort as I already had existing code for a simple rotation in CSS.

Below are animations on the profile and feed pages.


If you want to see rotating blue poos on Twitter in place of checkmarks for blue subscribers, you can install this extension.

  1. Get the code below, either by downloading or cloning
  2. Turn on developer mode in the top right
  3. Click "Load Unpacked Extension" and find the root of the project (the one with the manifest.json file in it)

If done correctly, you should see a blue poo in your extensions.


Get the Code

Check out the code here! The main files to look at are content.js and background.js. Read the section about those above to learn more about each of the responsibilities.

๐Ÿ“บ Watch me build it

You can watch the 8.5 hour video of me building it on Twitch. Be warned, a lot of it is me trying to figure it out. The first 2 hours I spent time planning, chatting, setting up and doing the profile page. The next 6.5 hours were spent figuring out how to get all the data for the feed.

Final Thoughts

Overall, this was a fun experiment. I am glad I was able to figure out a solution to the home feed poos, even though it only covered the first ~25 items.

The API is quite complex and was not the easiest thing to reverse engineer. Throwing it all together was an interesting challenge, and I'm glad I was able to solve the feed problem, even though it's not a full proper solution.

The UI was built with React and either styled components or another CSS-in-JS solution because there were a lot of elements with generated classes, so there were a lot of extra elements and CSS class names. Thankfully they've added some test ID's which helped with some selectors, though not all.