pull down to refresh

Twitter / X have user intent URLs so that you can auto-resolve from a user id to a profile url:

These work if you are logged in to twitter - but if you're not logged in they go to an error page.

If someone can help me find a way around this I'll pay the bounty.

Thanks! :)

25,000 sats bounty
MerryOscar's bounties

Use this https://tweeterid.com/

Or use twitter api if you willing to intergrate this in your app

I think this will work
GET https://api.twitter.com/1.1/users/show.json?user_id=ID

Replace ID with the user id

reply

Isn't it possible to just request get the website and parse the html statically?

reply

It seems like Twitter user intent URLs are designed to work when you're logged in, providing seamless redirection from user IDs to profile URLs. If you're not logged in, it may result in an error page possibly due to privacy or access restrictions

reply

Solved, with a working logged-out resolver. Your example, just now, from a terminal with no session:

$ node resolve_userid.mjs 2369450425
2369450425 -> https://x.com/MerryOscar (Oscar Merry)

Full source (~90 lines): https://paste.rs/imXDa (mirror: https://termbin.com/qxxd)

Why /i/user/<id> fails logged out: that route is a frontend redirect that only resolves inside an authenticated session — logged out, the SPA has no identity context and dumps you on the error/login page. The trick is to skip the frontend route and ask X's own GraphQL API directly, the same way the web app does. An anonymous visitor can obtain everything it needs:

  1. The public web bearer token — hard-coded in the app for years (it's in every JS bundle; not a secret).
  2. A guest tokenPOST https://api.twitter.com/1.1/guest/activate.json with that bearer returns a guest_token, no login, no cookies.
  3. An x-client-transaction-id header — since 2024 X requires this on GraphQL calls; it's derived from an animation-key embedded in the x.com home document (the ondemand.s obfuscation). I use the MIT-licensed x-client-transaction-id npm package which re-implements the web app's derivation — no browser needed.
  4. The current UserByRestId queryId — it rotates with every deploy, so the script scrapes it fresh from the client-web/main.*.js bundle each run (that's why hard-coded queryIds you find in old gists all 404 today).

Then one call:

GET https://api.twitter.com/graphql/<queryId>/UserByRestId
  ?variables={"userId":"2369450425","withSafetyModeUserFields":true}&features={...}&fieldToggles={...}
Authorization: Bearer <public web bearer>
x-guest-token: <from step 2>
x-client-transaction-id: <from step 3>

returns the user object; screen_name now lives under result.core.screen_name (it used to be legacy.screen_name — if you get undefined there, that's why). https://x.com/<screen_name> is your resolved profile URL.

Notes: this is rate-limited per guest token like normal web traffic (fine for occasional resolution, not bulk scraping); if a call starts 404ing, the queryId rotated — the script already handles that by re-scraping. No account, no API key, no login wall.