Skip to content
x

v0.5.0

A second free surface in x.com's own pages, one envelope on every record, a graph plane with edges, RDF and SQL, and the reads served over HTTP and MCP.

This is the largest release so far. x reads a second free surface, says where every field came from, turns what it reads into a graph you can query and export, and serves the same reads over HTTP and to an agent. Everything here is still free and still read-only.

x.com's own pages are a second Tier 0 surface

The website ships its data twice: a Relay store the client hydrates from, and schema.org microdata plus OpenGraph tags for everything else that crawls it. x now reads both, which is why a Tier 0 profile stopped being a cut-down profile. x user nasa with no credential gets the counters, the banner, the website, and what the verified tick means, and x tweet 20 gets the bookmark and view counts X took out of the embed years ago.

The same page carries the replies under a tweet, so x thread and x replies work with nothing at all. Going up a conversation was already free, because the syndication endpoint expands a reply's parent in full; going down now costs one page fetch instead of a session.

Every record says where it came from

One envelope on every record, in every format:

$ x tweet 20 -o json | jq -c '.[0] | {tier, surfaces, sources, via}'
{"tier":0,
 "surfaces":["s1","s8"],
 "sources":["https://cdn.syndication.twimg.com/tweet-result?id=20&token=6dq1&lang=en",
            "https://x.com/i/status/20"],
 "via":{"bookmarks":"s8","quotes":"s8","retweets":"s8"}}

surfaces is what answered, sources is the URLs it read, and via names the surface behind a field when more than one contributed. When a surface was tried and did not answer, missed says so, because a thin record from a spent rate window and a thin record from an account with nothing more to say are different facts.

x fields tweet prints the same thing as a census: every field, its type, and the surfaces measured to fill it, read out of the committed fixtures rather than declared by hand.

The graph plane: edges, RDF, and SQL

A read already knows things about other objects. x edges prints them without walking anywhere:

$ x edges 1903142823316049977 -o table --fields from,predicate,to
 FROM                           PREDICATE   TO
 x://tweet/1903142823316049977  replies_to  x://tweet/1903136743634723031
 x://tweet/1903142823316049977  mentions    x://user/jack
 x://tweet/1903142823316049977  mentions    x://user/marmoushera
 x://user/guyfishermoney        authored    x://tweet/1903142823316049977
 x://user/marmoushera           authored    x://tweet/1903136743634723031

One request, five claims, and the author of a tweet nobody fetched. x graph prints those claims together with the nodes they address, as one document. x rdf says the same thing in schema.org's vocabulary, which is not a taste call: X publishes schema.org microdata on its own pages, so a tweet already has a vendor-blessed RDF shape and this agrees with it instead of inventing a parallel one.

The local store keeps claims rather than the route the walk took, with the source as part of the key, so two surfaces asserting the same thing stay two rows and disagreement is queryable. x query runs SQL over it, x export --format writes the whole store as RDF, and --budget caps a crawl in requests rather than nodes, because requests are the unit the rate limits are written in.

serve and mcp carry the reads

x serve --addr :8080     # the reads as NDJSON under /v1/
x mcp                    # the same 24 as MCP tools, over stdio

Both take the global flags, so x serve --guest and x mcp --tier session serve at that tier and nothing else needs configuring. The walks and the credential commands stay on the command line: a crawl that writes to your disk and a command that saves your cookies are not things to hand a network port.

x trends and x places read a v1.1 route that still answers on the public web bearer, so both are Tier 0. x trends tokyo takes a name as well as a woeid, and x places is how you find the number; the directory caches for a week.

x space reads an audio Space whole: who created it, the admins and speakers, when it was scheduled, started and ended, and how many heard it live. It turned out a guest token reaches it, which took finding, because probing that operation with no variables answers 422 where the walled ones answer 404.

--tier means what it says

--tier is two flags in one string. A number caps what a run may use, so --tier 0 reads the way a machine with no credential would even when you have a session imported, which is how you check a Tier 0 claim for yourself. A name pins one surface: --tier syndication|oembed|web|guest|session. Anything else is a usage error listing the values, and so, now, is an -o you do not have: a typo that reads as if you had passed no flag at all is worse than a refusal.

Fixes

A profile timeline used to include tweets the account did not write. X renders a reply on a profile together with the tweet it answers, and x passed both through, so x timeline jack listed four other people and spent your -n on them. The timeline read now keeps the account's own posts. A repost is the case this cannot get right, since the page shows it under the original author and says nothing about who reposted it, so it drops with the reply parents.

An id that could not name a tweet now comes back as not found without asking X. x tweet 12345678901234567890 used to cost four requests and exit 1 with a line of JSON in it, because X answers 400 {"error":"Bad request."} there rather than a 404. A tweet id is a snowflake whose top 41 bits are the millisecond it was minted, so an id that decodes to the future is not an id. The message says which kind of not-found it is: a reader who mistyped an id should not be told the tweet used to be there.

Tests against the real thing

Three suites now, and they answer different questions. The fixture suite asks whether the parser still reads the bytes we captured. A golden suite asks whether it still produces the same record from them. A live suite, behind go test -tags live, asks whether those are still the bytes X sends, and it found the timeline defect on its first run. x capture is how the fixtures get refreshed, which is why none of them are hand-written.

Upgrade

go install github.com/tamnd/x-cli/cmd/x@latest
docker run --rm ghcr.io/tamnd/x:latest tweet 20

Prebuilt archives for Linux, macOS, and Windows on amd64 and arm64, plus deb, rpm, and apk packages, a container image, and signed checksums, are on the release page. The binary is pure Go, builds with CGO_ENABLED=0, and has no runtime dependencies.

Nothing was removed and no command changed its arguments. Two behaviours changed on purpose: a profile timeline no longer lists other people's tweets, and an output format x does not have is now a usage error instead of silently rendering jsonl.

License

x is derived from nitter and is licensed under the GNU AGPL-3.0.