// ==UserScript==
// @name Generate X-Client-Transaction-ID
// @version 2025-05-20
// @description JS code to generate required X-Client-Transaction-ID Header for X API requests
// @author You
// @match https://x.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant none
// @namespace https://greasyfork.org/users/1466972
// ==/UserScript==
(function() {
'use strict';
const savedFrames = [];
const ADDITIONAL_RANDOM_NUMBER = 3;
const DEFAULT_KEYWORD = "obfiowerehiring";
let defaultRowIndex = null;
let defaultKeyBytesIndices = null;
generateTID()
async function generateTID() {
if (!defaultRowIndex || !defaultKeyBytesIndices) {
const { firstIndex, remainingIndices } = await getIndices();
defaultRowIndex = firstIndex;
defaultKeyBytesIndices = remainingIndices;
}
const method = "GET"
const path = fetchApiURL()
const key = await getKey();
const keyBytes = getKeyBytes(key);
const animationKey = getAnimationKey(keyBytes);
const xTID = await getTransactionID(method, path, key, keyBytes, animationKey)
console.log("Generated Transaction ID: ", xTID)
}
function fetchApiURL() { // This can be made dynamic using message passing
return "/i/api/graphql/xd_EMdYvB9hfZsZ6Idri0w/TweetDetail"
}
const getFramesInterval = setInterval(() => {
const nodes = document.querySelectorAll('[id^="loading-x-anim"]');
if (nodes.length === 0 && savedFrames.length !== 0) {
clearInterval(getFramesInterval);
const serialized = savedFrames.map(node => node.outerHTML);
localStorage.setItem("savedFrames", JSON.stringify(serialized));
return;
}
nodes.forEach(removedNode => {
if (!savedFrames.includes(removedNode)) {
savedFrames.push(removedNode);
}
});
}, 10);
async function getIndices() {
let url = null;
const keyByteIndices = [];
const targetFileMatch = document.documentElement.innerHTML.match(/"ondemand\.s":"([0-9a-f]+)"/);
if (targetFileMatch) {
const hexString = targetFileMatch[1];
url = `https://abs.twimg.com/responsive-web/client-web/ondemand.s.${hexString}a.js`;
} else {
throw new Error("Transaction ID generator needs an update.");
}
const INDICES_REGEX = /\(\w{1}\[(\d{1,2})\],\s*16\)/g;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch indices file: ${response.statusText}`);
}
const jsContent = await response.text();
const keyByteIndicesMatch = [...jsContent.matchAll(INDICES_REGEX)];
keyByteIndicesMatch.forEach(item => {
keyByteIndices.push(item[1]);
});
if (keyByteIndices.length === 0) {
throw new Error("Couldn't get KEY_BYTE indices from file content");
}
const keyByteIndicesInt = keyByteIndices.map(Number);
return {
firstIndex: keyByteIndicesInt[0],
remainingIndices: keyByteIndicesInt.slice(1),
};
} catch (error) {
showError(error.message);
return null;
}
}
async function getKey() {
return new Promise(resolve => {
const meta = document.querySelector('meta[name="twitter-site-verification"]');
if (meta) resolve(meta.getAttribute("content"));
});
}
function getKeyBytes(key) {
return Array.from(atob(key).split("").map(c => c.charCodeAt(0)));
}
function getFrames() {
const stored = localStorage.getItem("savedFrames");
if (stored) {
const frames = JSON.parse(stored);
const parser = new DOMParser();
return frames.map(frame =>
parser.parseFromString(frame, "text/html").body.firstChild
);
}
return [];
}
function get2DArray(keyBytes) {
const frames = getFrames();
const array = Array.from(
frames[keyBytes[5] % 4].children[0].children[1]
.getAttribute("d")
.slice(9)
.split("C")
).map(item =>
item
.replace(/[^\d]+/g, " ")
.trim()
.split(" ")
.map(Number)
);
return array;
}
function solve(value, minVal, maxVal, rounding) {
const result = (value * (maxVal - minVal)) / 255 + minVal;
return rounding ? Math.floor(result) : Math.round(result * 100) / 100;
}
function animate(frames, targetTime) {
const fromColor = frames.slice(0, 3).concat(1).map(Number);
const toColor = frames.slice(3, 6).concat(1).map(Number);
const fromRotation = [0.0];
const toRotation = [solve(frames[6], 60.0, 360.0, true)];
const remainingFrames = frames.slice(7);
const curves = remainingFrames.map((item, index) =>
solve(item, isOdd(index), 1.0, false)
);
const cubic = new Cubic(curves);
const val = cubic.getValue(targetTime);
const color = interpolate(fromColor, toColor, val).map(value =>
value > 0 ? value : 0
);
const rotation = interpolate(fromRotation, toRotation, val);
const matrix = convertRotationToMatrix(rotation[0]);
const strArr = color.slice(0, -1).map(value =>
Math.round(value).toString(16)
);
for (const value of matrix) {
let rounded = Math.round(value * 100) / 100;
if (rounded < 0) {
rounded = -rounded;
}
const hexValue = floatToHex(rounded);
strArr.push(
hexValue.startsWith(".")
? `0${hexValue}`.toLowerCase()
: hexValue || "0"
);
}
const animationKey = strArr.join("").replace(/[.-]/g, "");
return animationKey;
}
function isOdd(num) {
return num % 2 !== 0 ? -1.0 : 0.0;
}
function getAnimationKey(keyBytes) {
const totalTime = 4096;
if (typeof defaultRowIndex === "undefined" || typeof defaultKeyBytesIndices === "undefined") {
throw new Error("Indices not initialized");
}
const rowIndex = keyBytes[defaultRowIndex] % 16;
const frameTime = defaultKeyBytesIndices.reduce((acc, index) => {
return acc * (keyBytes[index] % 16);
}, 1);
const arr = get2DArray(keyBytes);
if (!arr || !arr[rowIndex]) {
throw new Error("Invalid frame data");
}
const frameRow = arr[rowIndex];
const targetTime = frameTime / totalTime;
const animationKey = animate(frameRow, targetTime);
return animationKey;
}
async function getTransactionID(method, path, key, keyBytes, animationKey) {
if(!method || !path || !key || !animationKey) {
return console.log("Invalid call.")
}
const timeNow = Math.floor((Date.now() - 1682924400 * 1000) / 1000);
const timeNowBytes = [
timeNow & 0xff,
(timeNow >> 8) & 0xff,
(timeNow >> 16) & 0xff,
(timeNow >> 24) & 0xff,
];
const inputString = `${method}!${path}!${timeNow}${DEFAULT_KEYWORD}${animationKey}`;
const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(inputString));
const hashBytes = Array.from(new Uint8Array(hashBuffer));
const randomNum = Math.floor(Math.random() * 256);
const bytesArr = [
...keyBytes,
...timeNowBytes,
...hashBytes.slice(0, 16),
ADDITIONAL_RANDOM_NUMBER,
];
const out = new Uint8Array(bytesArr.length + 1);
out[0] = randomNum;
bytesArr.forEach((item, index) => {
out[index + 1] = item ^ randomNum;
});
const transactionId = btoa(String.fromCharCode(...out)).replace(/=+$/, "");
return transactionId;
}
class Cubic {
constructor(curves) {
this.curves = curves;
}
getValue(time) {
let startGradient = 0;
let endGradient = 0;
let start = 0.0;
let mid = 0.0;
let end = 1.0;
if (time <= 0.0) {
if (this.curves[0] > 0.0) {
startGradient = this.curves[1] / this.curves[0];
} else if (this.curves[1] === 0.0 && this.curves[2] > 0.0) {
startGradient = this.curves[3] / this.curves[2];
}
return startGradient * time;
}
if (time >= 1.0) {
if (this.curves[2] < 1.0) {
endGradient = (this.curves[3] - 1.0) / (this.curves[2] - 1.0);
} else if (this.curves[2] === 1.0 && this.curves[0] < 1.0) {
endGradient = (this.curves[1] - 1.0) / (this.curves[0] - 1.0);
}
return 1.0 + endGradient * (time - 1.0);
}
while (start < end) {
mid = (start + end) / 2;
const xEst = this.calculate(this.curves[0], this.curves[2], mid);
if (Math.abs(time - xEst) < 0.00001) {
return this.calculate(this.curves[1], this.curves[3], mid);
}
if (xEst < time) {
start = mid;
} else {
end = mid;
}
}
return this.calculate(this.curves[1], this.curves[3], mid);
}
calculate(a, b, m) {
return (
3.0 * a * (1 - m) * (1 - m) * m +
3.0 * b * (1 - m) * m * m +
m * m * m
);
}
}
function interpolate(fromList, toList, f) {
if (fromList.length !== toList.length) {
throw new Error("Invalid list");
}
const out = [];
for (let i = 0; i < fromList.length; i++) {
out.push(interpolateNum(fromList[i], toList[i], f));
}
return out;
}
function interpolateNum(fromVal, toVal, f) {
if (typeof fromVal === "number" && typeof toVal === "number") {
return fromVal * (1 - f) + toVal * f;
}
if (typeof fromVal === "boolean" && typeof toVal === "boolean") {
return f < 0.5 ? fromVal : toVal;
}
}
function convertRotationToMatrix(degrees) {
const radians = (degrees * Math.PI) / 180;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
return [cos, sin, -sin, cos, 0, 0];
}
function floatToHex(x) {
const result = [];
let quotient = Math.floor(x);
let fraction = x - quotient;
while (quotient > 0) {
quotient = Math.floor(x / 16);
const remainder = Math.floor(x - quotient * 16);
if (remainder > 9) {
result.unshift(String.fromCharCode(remainder + 55));
} else {
result.unshift(remainder.toString());
}
x = quotient;
}
if (fraction === 0) {
return result.join("");
}
result.push(".");
while (fraction > 0) {
fraction *= 16;
const integer = Math.floor(fraction);
fraction -= integer;
if (integer > 9) {
result.push(String.fromCharCode(integer + 55));
} else {
result.push(integer.toString());
}
}
return result.join("");
}
function base64Encode(array) {
return btoa(String.fromCharCode.apply(null, array));
}
})();