Skip to content

Instantly share code, notes, and snippets.

@mateuspontes
Last active June 29, 2025 20:27
Show Gist options
  • Save mateuspontes/5958a1af061d7666dc2b6a5ab00ed592 to your computer and use it in GitHub Desktop.
Save mateuspontes/5958a1af061d7666dc2b6a5ab00ed592 to your computer and use it in GitHub Desktop.
Twitter: unfavorite your tweets

Unfavorite Tweets Script

This script is designed to automate the process of unfavoriting tweets on X.com (formerly Twitter). It fetches the list of liked tweets for a user and removes the likes in batches, handling pagination and rate limits.

image

How to Use

  1. Access the Likes tab on X.com, e.g., https://x.com/xxxxxxxx/likes, replacing xxxxxxxx with your username.
  2. Open your browser's developer tools (usually by pressing F12 or Ctrl+Shift+I).
  3. Go to the Network tab and look for requests made to the X.com API.
  4. Copy the authorization and X-Client-Transaction-Id values from the request headers. Refer to the explanation provided in the original repository for more details.
  5. Replace the placeholders for authorization, client_tid, and other required variables in the script with the values you copied.
  6. Copy the script into your browser's developer console (tested on Google Chrome) or a Node.js environment.
  7. Run the script to start unfavoriting tweets.

Notes

  • The script uses the UnfavoriteTweet GraphQL endpoint to remove likes.
  • It respects API rate limits by introducing delays between requests.
  • Ensure you have the necessary permissions and credentials to access the X.com API.

Disclaimer

This script is for educational purposes only. Use it responsibly and ensure compliance with X.com’s terms of service.
I am not responsible for any misuse of this script. Use it at your own risk.

Inspiration

This script was inspired by the repository NietzscheKadse/XeetEntfernierer.

const authorization = 'Bearer ***********';
const client_tid = '**********';
const username = 'xxxxxxxxxxxx'; // Replace 'Username' with your X.com Username (But WITHOUT the @ !!!)
const ua = navigator.userAgentData.brands
.map((brand) => `"${brand.brand}";v="${brand.version}"`)
.join(', ');
const csrf_token = getCookie('ct0');
const random_resource = 'pBNL1QqK_TKItBqwTNBbeg'; // You may need to change this value!
const language_code = navigator.language.split('-')[0];
const user_id = getCookie('twid').substring(4);
let stop_signal = false;
function buildAcceptLanguageString() {
const languages = navigator.languages;
if (!languages || languages.length === 0) {
return 'en-US,en;q=0.9';
}
let q = 1;
const decrement = 0.1;
return languages
.map((lang) => {
if (q < 1) {
const result = `${lang};q=${q.toFixed(1)}`;
q -= decrement;
return result;
}
q -= decrement;
return lang;
})
.join(',');
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetch_liked_tweets(cursor = null, retry = 0) {
const count = '20';
const final_cursor = cursor ? `%22cursor%22%3A%22${cursor}%22%2C` : '';
const resource = 'fi2iAGkoLx5Iemj8rJZCSw';
const endpoint = 'Likes';
const base_url = `https://x.com/i/api/graphql/${resource}/${endpoint}`;
const variables = `?variables=%7B%22userId%22%3A%22${user_id}%22%2C%22count%22%3A${count}%2C${final_cursor}%22includePromotedContent%22%3Afalse%2C%22withClientEventToken%22%3Afalse%2C%22withBirdwatchNotes%22%3Afalse%2C%22withVoice%22%3Atrue%7D`;
const features = `&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Afalse%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D`;
const final_url = `${base_url}${variables}${features}`;
const response = await fetch(final_url, {
headers: {
accept: '*/*',
'accept-language': buildAcceptLanguageString(),
authorization: authorization,
'content-type': 'application/json',
'sec-ch-ua': ua,
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'x-client-transaction-id': client_tid,
'x-csrf-token': csrf_token,
'x-twitter-active-user': 'yes',
'x-twitter-auth-type': 'OAuth2Session',
'x-twitter-client-language': language_code,
},
method: 'GET',
mode: 'cors',
credentials: 'include',
});
if (!response.ok) {
if (response.status === 429) {
console.log('Rate limit reached. Waiting 1 minute.');
await sleep(1000 * 60);
return fetch_liked_tweets(cursor, retry + 1);
}
if (retry === 5) {
throw new Error('Max retries reached.');
}
console.log(
`(fetch_liked_tweets) Network response was not ok, status: ${response.status}, retrying in ${
10 * (1 + retry)
} seconds`
);
console.log(await response.text());
await sleep(10000 * (1 + retry));
return fetch_liked_tweets(cursor, retry + 1);
}
const data = await response.json();
const instructions =
data?.data?.user?.result?.timeline?.timeline?.instructions || [];
let entries = [];
for (const item of instructions) {
if (item.type === 'TimelineAddEntries') {
entries = item.entries;
break;
}
}
return entries;
}
async function unfavorite_tweets(tweet_ids) {
for (const tweet_id of tweet_ids) {
if (!tweet_id || isNaN(tweet_id)) {
console.log(`Skipping invalid tweet_id: ${tweet_id}`);
continue;
}
const response = await fetch(
'https://x.com/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet',
{
headers: {
accept: '*/*',
'accept-language': buildAcceptLanguageString(),
authorization: authorization,
'content-type': 'application/json',
'sec-ch-ua': ua,
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'x-client-transaction-id': client_tid,
'x-csrf-token': csrf_token,
'x-twitter-active-user': 'yes',
'x-twitter-auth-type': 'OAuth2Session',
'x-twitter-client-language': language_code,
},
body: JSON.stringify({
variables: { tweet_id },
queryId: 'ZYKSe-w7KEslx3JhSIk5LA',
}),
method: 'POST',
mode: 'cors',
credentials: 'include',
}
);
if (!response.ok) {
if (response.status === 429) {
console.log('Rate limit reached. Waiting 1 minute.');
await sleep(1000 * 60);
continue;
}
console.log(
`(unfavorite_tweets) Network response was not ok, status: ${response.status}`
);
console.log(await response.text());
continue;
}
console.log(`Unfavorited tweet ${tweet_id}`);
await sleep(100); // Delay to avoid rate limits
}
}
async function process_likes() {
let cursor = null;
while (!stop_signal) {
const entries = await fetch_liked_tweets(cursor);
if (!entries || entries.length === 0) {
console.log('No more liked tweets to process.');
break;
}
const tweet_ids = [];
for (const entry of entries) {
if (entry.entryId.startsWith('tweet-')) {
const tweet_id =
entry.content?.itemContent?.tweet_results?.result?.tweet?.rest_id ||
entry.content?.itemContent?.tweet_results?.result?.rest_id;
if (tweet_id && !isNaN(tweet_id)) {
tweet_ids.push(tweet_id);
} else {
console.log(`Invalid or missing tweet_id in entry: ${JSON.stringify(entry)}`);
}
} else if (entry.entryId.startsWith('cursor-bottom')) {
cursor = entry.content.value;
console.log(`Updated cursor: ${cursor}`);
} else {
console.log(`Unknown entry type: ${JSON.stringify(entry)}`);
}
}
if (tweet_ids.length > 0) {
console.log(`Processing ${tweet_ids.length} tweets: ${tweet_ids}`);
await unfavorite_tweets(tweet_ids);
} else {
console.log('No tweets to unfavorite in this batch.');
}
await sleep(3000); // Delay between batches
}
console.log('Unfavoriting process complete.');
}
process_likes();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment