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.
Trends, places, and Spaces
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.